diff --git a/changelog.d/fix-filing-requirement-6012a1-2026.fixed.md b/changelog.d/fix-filing-requirement-6012a1-2026.fixed.md new file mode 100644 index 00000000000..e2be297b9d9 --- /dev/null +++ b/changelog.d/fix-filing-requirement-6012a1-2026.fixed.md @@ -0,0 +1 @@ +Apply the 26 U.S.C. 6012(a)(1) federal filing requirement from 2026, when section 6012(f) expires: for a tax unit nobody can claim as a dependent, the threshold is the basic standard deduction plus the additions for age but not blindness, and gross income equal to it requires a return. diff --git a/changelog.d/fix-filing-requirement-dependents.fixed.md b/changelog.d/fix-filing-requirement-dependents.fixed.md new file mode 100644 index 00000000000..48846578166 --- /dev/null +++ b/changelog.d/fix-filing-requirement-dependents.fixed.md @@ -0,0 +1 @@ +Stop testing every filer's unearned income against $500 in the federal filing requirement (26 U.S.C. 6012): the unearned income test now applies only to a head or spouse who can be claimed as a dependent on another return, uses the section 63(c)(5)(A) amount for the year ($1,350 in 2025) plus that person's own additional standard deduction for age or blindness, and counts that person's gross income other than wages, business income and farm income (the earned income of the IRS dependent worksheet), so a self-employment loss no longer counts as unearned income. diff --git a/policyengine_us/parameters/gov/irs/income/filing_requirement/standard_deduction_rule/applies.yaml b/policyengine_us/parameters/gov/irs/income/filing_requirement/standard_deduction_rule/applies.yaml new file mode 100644 index 00000000000..c0ad170af25 --- /dev/null +++ b/policyengine_us/parameters/gov/irs/income/filing_requirement/standard_deduction_rule/applies.yaml @@ -0,0 +1,12 @@ +description: The Internal Revenue Service applies the section 6012(f) filing requirement, which compares gross income with the standard deduction, instead of section 6012(a)(1), if this is true. +values: + 2013-01-01: false + 2018-01-01: true + 2026-01-01: false +metadata: + unit: bool + period: year + label: IRS section 6012(f) filing requirement applies + reference: + - title: 26 U.S. Code § 6012(a)(1) and (f) + href: https://www.law.cornell.edu/uscode/text/26/6012#f diff --git a/policyengine_us/tests/core/test_filing_requirement_dependent_invariants.py b/policyengine_us/tests/core/test_filing_requirement_dependent_invariants.py index ddb9c56bba1..9b895cdd293 100644 --- a/policyengine_us/tests/core/test_filing_requirement_dependent_invariants.py +++ b/policyengine_us/tests/core/test_filing_requirement_dependent_invariants.py @@ -40,7 +40,7 @@ loss, interest and dividends) and runs each batch, with four variants of every unit, as one simulation for 2025 and 2026. Every batch starts with two fixed units, one that must file and one that need not, so that each property below -has rows to check and the batch holds both answers; the test asserts both. +has rows to check and both answers occur; the test asserts that it does. 5. Split invariance: moving a filer's wages into interest, or interest into wages, never changes the result for a tax unit whose head and spouse cannot @@ -73,15 +73,22 @@ instructions, Chart A) but it does reduce the earned income that sets the limited standard deduction, so a smaller loss raises the threshold and not the income. `test_smaller_loss_can_end_the_requirement` pins the case. -- Property 7 leaves out blind filers. The model counts the additional - standard deduction for blindness in every filer's threshold. Chart A does - not (only age), while Table 2 does, so on the charts a blind filer under 65 - with wages between the two amounts must file only if nobody can claim them. - The property holds in the model today, and leaving blind filers out keeps it - true under either reading. It also leaves out separate filers: Table 2 lets - a claimable married dependent filing separately skip a return unless the - spouse itemizes, while sections 6012(f) and (a)(1) require one with any - gross income (decision d1395). +- Property 7 leaves out blind filers. Chart A does not count blindness (only + age), while Table 2 does, so on the charts a blind filer under 65 with + wages between the two amounts must file only if nobody can claim them. From + 2026 the model follows Chart A here: section 6012(a)(1)(B) raises the + threshold of a filer nobody can claim for age only, while a filer who can + be claimed keeps the standard deduction test (decision d1395). +- Property 7 also leaves out separate filers: Table 2 lets a claimable + married dependent filing separately skip a return unless the spouse + itemizes, while sections 6012(f) and (a)(1) require one with any gross + income (decision d1395). +- From 2026, property 7 also fails at one dollar: gross income equal to the + threshold of a sighted filer nobody can claim. Section 6012(a)(1)(A) + excuses that filer only if gross income is "less than" the threshold, while + a filer who can be claimed is tested with "in excess of" (d1395). The + property skips those units, and + `test_claimability_can_end_the_requirement_at_the_threshold` pins the case. - Properties 1, 2 and 9 use wages as the only earned income. Earned income for the limited deduction is net of the deductible part of self-employment tax on the IRS worksheet, which Table 2 does not spell out. @@ -129,8 +136,8 @@ def table_2_single_dependent(earned, unearned, aged, blind): return unearned_test | earned_test | gross_test -def simulate(units): - """One simulation of independent tax units. +def simulate(units, **options): + """One simulation of independent tax units; options go to Simulation. Each unit is {"filing_status": name, "adults": [...], "children": [...]}, with one dict of person inputs per member. Every unit states its filing @@ -168,7 +175,8 @@ def simulate(units): "tax_units": tax_units, "households": households, **groups, - } + }, + **options, ) @@ -192,6 +200,13 @@ def head_values(units, name): return np.array([unit["adults"][0].get(name, 0) for unit in units]) +def unit_totals(simulation, units, variable, year): + """A person variable summed over the members of each unit.""" + values = np.asarray(simulation.calculate(variable, year)) + sizes = [len(unit["adults"]) + len(unit["children"]) for unit in units] + return np.add.reduceat(values, np.cumsum([0] + sizes[:-1])) + + def head_medicaid_requirement(simulation, units, year): """`medicaid_person_is_required_to_file` for each unit's head.""" values = simulation.calculate("medicaid_person_is_required_to_file", year) @@ -483,9 +498,18 @@ def test_filing_requirement_properties(anchors, drawn): f"{year}: more income ended the requirement" ) assert required["base"].any() and not required["base"].all() - # Property 7. + # Property 7, except at the 2026 threshold (see the docstring). sighted = ~anyone_blind[:, 0] & ~separate[:, 0] - assert not (required["base"] & ~required["claimed"])[sighted].any(), ( + ended = required["base"] & ~required["claimed"] + if year >= 2026: + gross_income = unit_totals( + simulation, units, "irs_gross_income", year + ).reshape(-1, len(VARIANTS))[:, 0] + standard_deduction = np.asarray( + simulation.calculate("standard_deduction", year) + ).reshape(-1, len(VARIANTS))[:, 0] + ended &= ~(nobody_claimable & (gross_income == standard_deduction)) + assert not ended[sighted].any(), ( f"{year}: being claimable ended the requirement" ) # Property 8. @@ -526,3 +550,14 @@ def test_smaller_loss_can_end_the_requirement(): units[1]["adults"][0]["self_employment_income"] = -100 required = simulate(units).calculate("tax_unit_is_required_to_file", 2025) assert required.tolist() == [True, False] + + +def test_claimability_can_end_the_requirement_at_the_threshold(): + # Intended exception to property 7 from 2026. Nobody can claim the first + # filer, whose $16,100 of wages equals the threshold of section + # 6012(a)(1)(A)(i), so a return is due. The second filer can be claimed; + # $16,100 does not exceed their standard deduction of $16,100 + # ((a)(1)(C)(i)(II) and Chart B), so no return is due (decision d1395). + units = [single_filer(40, False, claimed, 16_100, 0) for claimed in (0, 1)] + required = simulate(units).calculate("tax_unit_is_required_to_file", 2026) + assert required.tolist() == [True, False] diff --git a/policyengine_us/tests/core/test_filing_requirement_invariants.py b/policyengine_us/tests/core/test_filing_requirement_invariants.py new file mode 100644 index 00000000000..6c54dc4ff9c --- /dev/null +++ b/policyengine_us/tests/core/test_filing_requirement_invariants.py @@ -0,0 +1,409 @@ +"""Invariants for the filing requirement of a tax unit nobody can claim. + +`tax_unit_is_required_to_file` applies 26 USC 6012(f) for 2018 to 2025 and +6012(a)(1) in other years; the parameter +gov.irs.income.filing_requirement.standard_deduction_rule.applies switches +between them. YAML cases pin the published amounts one case at a time; these +tests check what must hold for every input, which YAML cannot state. + +The reference rules below are computed from the published amounts, not from +the model's parameters: + +- 6012(a)(1)(A) and (B): a return is required when gross income is at least + the basic standard deduction plus one additional standard deduction for + each spouse aged 65 or older. Blindness does not count. +- 6012(f)(1) and (2)(A): a return is required when gross income exceeds the + standard deduction, which adds the additional standard deduction for age + and for blindness. +- Under both, a married individual filing separately must file with any + gross income. + +`test_threshold_grid` runs one simulation over every filing status, every +combination of age 65 and blindness for the head and spouse, and gross income +of $0, $1, and a dollar under, at and over each rule's threshold: + +1. Differential: the 2026 result equals 6012(a)(1) with the 2026 amounts. +2. Differential: the 2025 result equals 6012(f) with the 2025 amounts. + +`test_filing_requirement_properties` draws batches of tax units with +Hypothesis (single, head of household, surviving spouse, separate and joint; +any mix of age, blindness, claimability, wages, interest, dividends, and +self-employment profit or loss; gross income often at a threshold) and runs +each batch, with two variants of every unit, as one simulation: + +3. Differentials 1 and 2 on every drawn unit nobody can claim. +4. Blindness: for a unit nobody can claim, it never changes the 2026 result. + For every other unit and year, more blindness never adds a requirement. +5. Age: reaching 65 never adds a requirement, for any unit or year. Every + unit supplies its roles (the first adult heads it), so a variant cannot + change who is head: with roles left to age order, raising both spouses to + 65 can make a claimable spouse the head, which limits the joint standard + deduction for a reason that has nothing to do with age. +6. Inclusion: in 2026, the model never excuses a unit nobody can claim that + 6012(f), with the 2026 amounts, would require to file. +7. Claimable units keep one rule in both years: for a unit whose head or + spouse can be claimed, a return is required exactly when gross income is + over the unit's standard deduction (any gross income on a separate + return), or a claimable adult's interest and dividends are over the + section 63(c)(5)(A) amount ($1,350 in 2025 and 2026) plus that adult's + own additions for age and blindness. How 6012(a)(1) applies to these + filers is decision d1395, so the change leaves them alone. + +The tests use the default tax-benefit system only. A reform that kept 6012(f) +for 2026 would compare the two rules directly, but it costs a second +parameter tree (about 0.7 GB) in the CI group, so properties 6 and 7 compare +the model with references instead. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us.tests.core.test_filing_requirement_dependent_invariants import ( # noqa: E501 + simulate, +) + +YEARS = (2025, 2026) +STATUSES = ["SINGLE", "HEAD_OF_HOUSEHOLD", "SURVIVING_SPOUSE", "SEPARATE", "JOINT"] + +# Basic standard deduction and additional standard deduction by status. +# 2025: Chart A of the 2025 Form 1040 instructions (single $15,750, or +# $17,750 at 65; joint $31,500, $33,100 and $34,700; head of household +# $23,625 and $25,625), https://www.irs.gov/pub/irs-prior/i1040gi--2025.pdf +# 2026: Rev. Proc. 2025-32, sec. 4.14, +# https://www.irs.gov/pub/irs-drop/rp-25-32.pdf#page=18 +BASIC = { + 2025: { + "SINGLE": 15_750, + "HEAD_OF_HOUSEHOLD": 23_625, + "SURVIVING_SPOUSE": 31_500, + "SEPARATE": 15_750, + "JOINT": 31_500, + }, + 2026: { + "SINGLE": 16_100, + "HEAD_OF_HOUSEHOLD": 24_150, + "SURVIVING_SPOUSE": 32_200, + "SEPARATE": 16_100, + "JOINT": 32_200, + }, +} +UNMARRIED = ("SINGLE", "HEAD_OF_HOUSEHOLD") +ADDITIONAL = { + 2025: {status: 2_000 if status in UNMARRIED else 1_600 for status in STATUSES}, + 2026: {status: 2_050 if status in UNMARRIED else 1_650 for status in STATUSES}, +} + +GROSS_INCOME_ITEMS = [ + "employment_income", + "self_employment_income", + "taxable_interest_income", + "qualified_dividend_income", +] +# Section 63(c)(5)(A) amount: Rev. Proc. 2024-40, sec. 2.15(2), and Rev. Proc. +# 2025-32, sec. 4.14(2). +DEPENDENT_AMOUNT = {2025: 1_350, 2026: 1_350} +UNEARNED_ITEMS = ["taxable_interest_income", "qualified_dividend_income"] +VARIANTS = ["base", "blindness_flipped", "aged"] + + +def gross_income(unit): + """Gross income of the head and spouse: no loss reduces it.""" + return sum( + max(0, adult.get(item, 0)) + for adult in unit["adults"] + for item in GROSS_INCOME_ITEMS + ) + + +def threshold(unit, year, rule): + """The reference threshold, or None for a separate return.""" + status = unit["filing_status"] + if status == "SEPARATE": + return None + aged = sum(adult["age"] >= 65 for adult in unit["adults"]) + blind = sum(adult["is_blind"] for adult in unit["adults"]) + additions = aged if rule == "6012(a)(1)" else aged + blind + return BASIC[year][status] + additions * ADDITIONAL[year][status] + + +def reference(unit, year, rule): + """Whether the reference rule requires a return.""" + income = gross_income(unit) + limit = threshold(unit, year, rule) + if limit is None: + return income > 0 + if rule == "6012(a)(1)": + return income >= limit + return income > limit + + +def claimable_unearned_test(unit, year): + """Whether a claimable adult's interest and dividends are over the + section 63(c)(5)(A) amount plus that adult's own additions.""" + addition = ADDITIONAL[year][unit["filing_status"]] + return any( + adult["claimed_as_dependent_on_another_return"] + and sum(max(0, adult.get(item, 0)) for item in UNEARNED_ITEMS) + > DEPENDENT_AMOUNT[year] + addition * ((adult["age"] >= 65) + adult["is_blind"]) + for adult in unit["adults"] + ) + + +def rule_in_force(year): + return "6012(f)" if year <= 2025 else "6012(a)(1)" + + +def nobody_claimable(unit): + return not any( + adult.get("claimed_as_dependent_on_another_return", False) + for adult in unit["adults"] + ) + + +def adult(age, blind, claimed=False, **income): + return { + "age": age, + "is_blind": blind, + "claimed_as_dependent_on_another_return": claimed, + **income, + } + + +def tax_unit(status, adults): + """A tax unit whose first adult is the head and second the spouse.""" + for n, person in enumerate(adults): + person["tax_unit_role_input"] = "SPOUSE" if n else "HEAD" + return {"filing_status": status, "adults": adults, "children": []} + + +def with_gross_income(status, ages, blindness, income): + """A unit nobody can claim, its gross income split between the adults.""" + adults = [adult(age, blind) for age, blind in zip(ages, blindness)] + adults[0]["employment_income"] = income // 2 + adults[-1]["taxable_interest_income"] = ( + adults[-1].get("taxable_interest_income", 0) + income - income // 2 + ) + return tax_unit(status, adults) + + +def test_threshold_grid(): + units = [] + for status in STATUSES: + size = 2 if status == "JOINT" else 1 + for ages in np.ndindex(*([2] * size)): + ages = [65 + 5 * flag if flag else 40 for flag in ages] + for blindness in np.ndindex(*([2] * size)): + blindness = [bool(flag) for flag in blindness] + probe = with_gross_income(status, ages, blindness, 0) + incomes = {0, 1} + for year in YEARS: + for rule in ("6012(a)(1)", "6012(f)"): + limit = threshold(probe, year, rule) + if limit is not None: + incomes |= {limit - 1, limit, limit + 1} + units += [ + with_gross_income(status, ages, blindness, income) + for income in sorted(incomes) + ] + simulation = simulate(units) + for year in YEARS: + required = np.asarray( + simulation.calculate("tax_unit_is_required_to_file", year) + ) + expected = np.array( + [reference(unit, year, rule_in_force(year)) for unit in units] + ) + mismatched = [ + (unit["filing_status"], gross_income(unit), unit["adults"]) + for unit in np.array(units, dtype=object)[required != expected] + ] + assert not mismatched, f"{year}: {mismatched[:3]}" + # Both answers occur, so the check is not vacuous. + assert required.any() and not required.all() + + +# Gross incomes at each reference threshold, a dollar under and over. +BOUNDARIES = sorted( + { + BASIC[year][status] + count * ADDITIONAL[year][status] + offset + for year in YEARS + for status in STATUSES + for count in range(5) + for offset in (-1, 0, 1) + } +) + + +@st.composite +def units(draw): + status = draw(st.sampled_from(STATUSES)) + size = 2 if status == "JOINT" else 1 + claimable = draw(st.booleans()) + adults = [ + adult( + age=draw(st.sampled_from([19, 40, 64, 65, 66, 80])), + blind=draw(st.booleans()), + claimed=claimable and draw(st.booleans()), + ) + for _ in range(size) + ] + # Gross income, often exactly at a threshold, split among the adults' + # nonnegative items. A self-employment loss does not reduce it. + total = draw( + st.one_of( + st.sampled_from([0, 1] + BOUNDARIES), + st.integers(0, 45_000), + ) + ) + slots = [(n, item) for n in range(size) for item in GROSS_INCOME_ITEMS] + weights = [draw(st.integers(0, 3)) for _ in slots] + if not any(weights): + weights[0] = 1 + shares = [total * weight // sum(weights) for weight in weights] + shares[weights.index(max(weights))] += total - sum(shares) + for (n, item), share in zip(slots, shares): + adults[n][item] = share + loser = draw(st.integers(0, size)) + if loser < size and adults[loser]["self_employment_income"] == 0: + adults[loser]["self_employment_income"] = -draw(st.integers(1, 20_000)) + return tax_unit(status, adults) + + +def variants_of(base): + """A tax unit and its variants, in VARIANTS order.""" + + def variant(change): + copy = {**base, "adults": [dict(adult) for adult in base["adults"]]} + for person in copy["adults"]: + change(person) + return copy + + def flip_blindness(person): + person["is_blind"] = not person["is_blind"] + + def reach_65(person): + person["age"] = max(person["age"], 65) + + return [base, variant(flip_blindness), variant(reach_65)] + + +@st.composite +def unit_variants(draw): + return variants_of(draw(units())) + + +# Every example holds a unit nobody can claim, at the 2026 threshold of a blind +# filer; a unit whose head can be claimed, over the unearned income test; and +# a joint unit headed by a younger, claimable adult. Were roles left to age +# order, that adult would head the unit only once both are 65 (the case that +# needs the roles fixed). +ANCHORS = [ + variants_of(tax_unit("SINGLE", [adult(40, True, employment_income=16_100)])), + variants_of( + tax_unit("SINGLE", [adult(19, False, True, taxable_interest_income=2_000)]) + ), + variants_of( + tax_unit( + "JOINT", + [ + adult(19, False, True), + adult(40, False, taxable_interest_income=10_000), + ], + ) + ), +] + + +def blind_count(unit): + return sum(adult["is_blind"] for adult in unit["adults"]) + + +@settings( + max_examples=6, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given(drawn=st.lists(unit_variants(), min_size=8, max_size=24)) +def test_filing_requirement_properties(drawn): + drawn = ANCHORS + drawn + flat = [unit for variants in drawn for unit in variants] + simulation = simulate(flat) + by_variant = { + name: [variants[n] for variants in drawn] for n, name in enumerate(VARIANTS) + } + unclaimable = np.array([nobody_claimable(unit) for unit in by_variant["base"]]) + # Blind adults in the flipped variant less those in the base unit. On a + # joint return with one blind spouse, flipping swaps who is blind. + blindness_change = np.array( + [ + blind_count(flipped) - blind_count(base) + for base, flipped in zip( + by_variant["base"], by_variant["blindness_flipped"] + ) + ] + ) + + def results(variable, year): + values = simulation.calculate(variable, year) + columns = np.asarray(values).reshape(-1, len(VARIANTS)).T + return dict(zip(VARIANTS, columns)) + + for year in YEARS: + required = results("tax_unit_is_required_to_file", year) + base = required["base"] + # Property 3. + expected = np.array( + [reference(unit, year, rule_in_force(year)) for unit in by_variant["base"]] + ) + np.testing.assert_array_equal( + base[unclaimable], + expected[unclaimable], + err_msg=f"{year}: differs from {rule_in_force(year)}", + ) + # Property 4. + flipped = required["blindness_flipped"] + if year >= 2026: + np.testing.assert_array_equal( + flipped[unclaimable], + base[unclaimable], + err_msg=f"{year}: blindness changed the result", + ) + checked = ~unclaimable if year >= 2026 else np.ones_like(unclaimable) + gained = ((blindness_change > 0) & flipped & ~base) | ( + (blindness_change < 0) & base & ~flipped + ) + assert not (gained & checked).any(), ( + f"{year}: more blindness added a requirement" + ) + # Property 5. + assert not (required["aged"] & ~base).any(), ( + f"{year}: reaching 65 added a requirement" + ) + # Property 6. + if year >= 2026: + required_by_f = np.array( + [reference(unit, year, "6012(f)") for unit in by_variant["base"]] + ) + assert not (required_by_f & ~base)[unclaimable].any(), ( + f"{year}: excused a unit 6012(f) requires" + ) + # Property 7. + standard_deduction = results("standard_deduction", year)["base"] + expected_claimable = np.array( + [ + ( + gross_income(unit) > 0 + if unit["filing_status"] == "SEPARATE" + else gross_income(unit) > deduction + ) + or claimable_unearned_test(unit, year) + for unit, deduction in zip(by_variant["base"], standard_deduction) + ] + ) + np.testing.assert_array_equal( + base[~unclaimable], + expected_claimable[~unclaimable], + err_msg=f"{year}: the rule for claimable units changed", + ) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax_unit_is_required_to_file.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax_unit_is_required_to_file.yaml index 839bb09c946..2804058799c 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax_unit_is_required_to_file.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax_unit_is_required_to_file.yaml @@ -1,3 +1,438 @@ +# Federal filing requirement for a tax unit nobody can claim as a dependent. +# +# 2018 to 2025, 26 U.S.C. 6012(f): no return is required of an unmarried +# individual whose gross income "does not exceed the standard deduction +# applicable to such individual ... under section 63", or of a couple whose +# combined gross income does not exceed the joint standard deduction. The +# standard deduction includes the additional standard deduction for age and +# for blindness (section 63(c)(1) and (f)). +# +# From 2026, 26 U.S.C. 6012(a)(1): subsection (f) covers only taxable years +# "before January 1, 2026", and Pub. L. 119-21 does not amend section 6012. +# No return is required when gross income is "less than" the exemption amount +# (zero, section 151(d)(5)) plus the basic standard deduction. Subparagraph +# (B) raises that by one additional standard deduction for each spouse aged +# 65 or older, "by reason of section 63(f)(1)", and not for blindness. A +# married individual filing separately has no exception. +# https://www.law.cornell.edu/uscode/text/26/6012 +# +# 2026 amounts, Rev. Proc. 2025-32, sec. 4.14: basic standard deduction +# $16,100 single, $32,200 joint and surviving spouse, $24,150 head of +# household; additional standard deduction $1,650, or $2,050 if unmarried and +# not a surviving spouse. https://www.irs.gov/pub/irs-drop/rp-25-32.pdf#page=18 +# Chart A of the 2017 Form 1040 instructions, the last year under (a)(1), has +# the same form: age only, and "at least". +# https://www.irs.gov/pub/irs-prior/i1040gi--2017.pdf#page=8 +# +# 2025 amounts, Rev. Proc. 2024-40 as amended by Pub. L. 119-21 sec. 70102: +# $15,750 single, $31,500 joint, $23,625 head of household; additional +# standard deduction $1,600, or $2,000 if unmarried and not a surviving spouse. + +- name: Case 1, 2026, single filer under 65 with gross income a dollar under $16,100. + period: 2026 + input: + filing_status: SINGLE + age: 40 + taxable_interest_income: 16_099 + output: + tax_unit_is_required_to_file: false + +- name: Case 2, 2026, single filer under 65 with gross income of exactly $16,100. + period: 2026 + input: + filing_status: SINGLE + age: 40 + taxable_interest_income: 16_100 + output: + # (a)(1)(A)(i): excused only if gross income is less than $16,100. + tax_unit_is_required_to_file: true + +- name: Case 3, 2026, blind single filer under 65 with gross income of $16,100. + period: 2026 + input: + filing_status: SINGLE + age: 40 + is_blind: true + employment_income: 16_100 + output: + # Blindness does not raise the (a)(1) threshold. + tax_unit_is_required_to_file: true + +- name: Case 4, 2026, blind single filer under 65 with gross income a dollar under $16,100. + period: 2026 + input: + filing_status: SINGLE + age: 40 + is_blind: true + employment_income: 16_099 + output: + tax_unit_is_required_to_file: false + +- name: Case 5, 2026, single filer aged 65 with gross income a dollar under $18,150. + period: 2026 + input: + filing_status: SINGLE + age: 65 + taxable_interest_income: 18_149 + output: + # $16,100 + $2,050. + tax_unit_is_required_to_file: false + +- name: Case 6, 2026, single filer aged 65 with gross income of exactly $18,150. + period: 2026 + input: + filing_status: SINGLE + age: 65 + taxable_interest_income: 18_150 + output: + tax_unit_is_required_to_file: true + +- name: Case 7, 2026, blind single filer aged 65 gets only the age addition. + period: 2026 + input: + filing_status: SINGLE + age: 70 + is_blind: true + taxable_interest_income: 18_150 + output: + # $16,100 + $2,050 for age; nothing for blindness. + tax_unit_is_required_to_file: true + +- name: Case 8, 2026, joint return under 65 with combined gross income a dollar under $32,200. + period: 2026 + input: + people: + person1: + age: 40 + employment_income: 20_000 + person2: + age: 40 + taxable_interest_income: 12_199 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: TX + output: + filing_status: JOINT + tax_unit_is_required_to_file: false + +- name: Case 9, 2026, joint return under 65 with combined gross income of exactly $32,200. + period: 2026 + input: + people: + person1: + age: 40 + employment_income: 20_000 + person2: + age: 40 + taxable_interest_income: 12_200 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: TX + output: + # (a)(1)(A)(iv): excused only if combined gross income is less than + # $32,200. + tax_unit_is_required_to_file: true + +- name: Case 10, 2026, joint return with one spouse aged 65 and gross income of $33,849. + period: 2026 + input: + people: + person1: + age: 67 + taxable_interest_income: 33_849 + person2: + age: 60 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: TX + output: + # $32,200 + $1,650. + tax_unit_is_required_to_file: false + +- name: Case 11, 2026, joint return with one spouse aged 65 and gross income of $33,850. + period: 2026 + input: + people: + person1: + age: 67 + taxable_interest_income: 33_850 + person2: + age: 60 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: TX + output: + tax_unit_is_required_to_file: true + +- name: Case 12, 2026, joint return with both spouses aged 65 and gross income of $35,499. + period: 2026 + input: + people: + person1: + age: 70 + taxable_interest_income: 35_499 + person2: + age: 68 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: TX + output: + # $32,200 + 2 x $1,650. + tax_unit_is_required_to_file: false + +- name: Case 13, 2026, joint return with both spouses aged 65 and blind at $35,500. + period: 2026 + input: + people: + person1: + age: 70 + is_blind: true + taxable_interest_income: 35_500 + person2: + age: 68 + is_blind: true + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: TX + output: + # Two age additions only. The standard deduction is $38,800 with the two + # blindness additions, which (a)(1)(B) leaves out. + standard_deduction: 38_800 + tax_unit_is_required_to_file: true + +- name: Case 14, 2026, head of household aged 65 with gross income of $26,199. + period: 2026 + input: + filing_status: HEAD_OF_HOUSEHOLD + age: 66 + taxable_interest_income: 26_199 + output: + # $24,150 + $2,050. + tax_unit_is_required_to_file: false + +- name: Case 15, 2026, head of household aged 65 with gross income of $26,200. + period: 2026 + input: + filing_status: HEAD_OF_HOUSEHOLD + age: 66 + taxable_interest_income: 26_200 + output: + tax_unit_is_required_to_file: true + +- name: Case 16, 2026, surviving spouse aged 65 with gross income of $33,849. + period: 2026 + input: + filing_status: SURVIVING_SPOUSE + age: 66 + taxable_interest_income: 33_849 + output: + # $32,200 + $1,650: a surviving spouse takes the married addition. + tax_unit_is_required_to_file: false + +- name: Case 17, 2026, surviving spouse aged 65 with gross income of $33,850. + period: 2026 + input: + filing_status: SURVIVING_SPOUSE + age: 66 + taxable_interest_income: 33_850 + output: + tax_unit_is_required_to_file: true + +- name: Case 18, 2026, married individual filing separately with $1 of gross income. + period: 2026 + input: + filing_status: SEPARATE + age: 70 + is_blind: true + taxable_interest_income: 1 + output: + # The spouse files a separate return, so (a)(1)(A)(iv) does not apply, + # and the exemption amount is zero. Age and blindness do not matter. + tax_unit_is_required_to_file: true + +- name: Case 19, 2026, married individual filing separately with no gross income. + period: 2026 + input: + filing_status: SEPARATE + age: 40 + output: + # With a zero exemption amount, (a)(1)(A) read literally reaches even + # $0 of gross income; the IRS set $5 for 2018 to 2025. The model, as + # before, requires a return only for positive gross income (decision + # d1403). + tax_unit_is_required_to_file: false + +- name: Case 20, 2025, single filer under 65 with gross income of exactly $15,750. + period: 2025 + input: + filing_status: SINGLE + age: 40 + taxable_interest_income: 15_750 + output: + # (f)(1): gross income does not exceed the standard deduction. Chart A of + # the 2025 instructions says "at least" $15,750 (decision d1403). + tax_unit_is_required_to_file: false + +- name: Case 21, 2025, single filer under 65 with gross income of $15,751. + period: 2025 + input: + filing_status: SINGLE + age: 40 + taxable_interest_income: 15_751 + output: + tax_unit_is_required_to_file: true + +- name: Case 22, 2025, blind single filer under 65 with gross income of $17,750. + period: 2025 + input: + filing_status: SINGLE + age: 40 + is_blind: true + employment_income: 17_750 + output: + # (f)(1): the standard deduction is $15,750 + $2,000 for blindness. + # Chart A of the 2025 instructions lists only age (decision d1403). + tax_unit_is_required_to_file: false + +- name: Case 23, 2025, blind single filer under 65 with gross income of $17,751. + period: 2025 + input: + filing_status: SINGLE + age: 40 + is_blind: true + employment_income: 17_751 + output: + tax_unit_is_required_to_file: true + +- name: Case 24, 2025, joint return with both spouses aged 65 and blind at $37,900. + period: 2025 + input: + people: + person1: + age: 70 + is_blind: true + taxable_interest_income: 37_900 + person2: + age: 68 + is_blind: true + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: TX + output: + # (f)(2)(A): $31,500 + 4 x $1,600. + tax_unit_is_required_to_file: false + +- name: Case 25, 2025, joint return with both spouses aged 65 and blind at $37,901. + period: 2025 + input: + people: + person1: + age: 70 + is_blind: true + taxable_interest_income: 37_901 + person2: + age: 68 + is_blind: true + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: TX + output: + tax_unit_is_required_to_file: true + +- name: Case 26, 2025, married individual filing separately with $1 of gross income. + period: 2025 + input: + filing_status: SEPARATE + age: 70 + is_blind: true + taxable_interest_income: 1 + output: + # (f) excuses only unmarried individuals and joint filers. + tax_unit_is_required_to_file: true + +- name: Case 27, 2025, married individual filing separately with no gross income. + period: 2025 + input: + filing_status: SEPARATE + age: 40 + output: + tax_unit_is_required_to_file: false + +# A tax unit whose head or spouse can be claimed as a dependent keeps the +# standard deduction test from 2026 too: (a)(1)(C)(i)(II), "total gross income +# in excess of the standard deduction", which is what Chart B applies. +# Whether the (A) threshold, which leaves out blindness and requires a return +# at equality, also binds these filers is decision d1395. These cases pin the +# model's reading until it is ruled. + +- name: Case 28, 2026, blind single filer who can be claimed keeps the blindness addition. + period: 2026 + input: + filing_status: SINGLE + age: 19 + is_blind: true + claimed_as_dependent_on_another_return: true + employment_income: 17_000 + output: + # Limited standard deduction min($16,100, $17,000 + $450) + $2,050 = + # $18,150, over gross income. Nobody can claim the filer in Case 3. + standard_deduction: 18_150 + tax_unit_is_required_to_file: false + +- name: Case 29, 2026, joint return whose spouse can be claimed, at exactly the joint amount. + period: 2026 + input: + people: + person1: + age: 40 + employment_income: 32_200 + person2: + age: 40 + claimed_as_dependent_on_another_return: true + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: TX + output: + # Gross income does not exceed the $32,200 standard deduction. Compare + # Case 9, where nobody can be claimed. + tax_unit_is_required_to_file: false + # 26 U.S.C. 6012(c): for the filing requirement, gross income is computed # without regard to the section 911 exclusion. With Form 2555 leaf inputs, # that is Form 2555 line 43 (the line 36 housing exclusion plus the line 42 @@ -118,3 +553,23 @@ output: # $10,000 of gross income, under $15,750, and none of it is unearned. tax_unit_is_required_to_file: false + +- name: Gross section 911 exclusion reaches the 2026 age-only threshold for a blind filer + period: 2026 + input: + filing_status: SINGLE + age: 40 + is_blind: true + employment_income: 10_000 + foreign_housing_exclusion: 6_100 + foreign_earned_income_exclusion_allocable_deductions: 6_100 + output: + # Line 2c = max(0, $6,100 - $6,100) = $0; line 43 = $6,100. + foreign_earned_income_exclusion: 0 + foreign_earned_income_exclusion_gross: 6_100 + # Section 6012(c): $10,000 + $6,100 = $16,100 of filing gross income. + # Section 6012(a)(1): $16,100 >= $16,100 basic + $0 age addition. + # Blindness does not raise the filing threshold; the standard deduction + # is $16,100 + $2,050 = $18,150 and would not require a return under (f). + standard_deduction: 18_150 + tax_unit_is_required_to_file: true diff --git a/policyengine_us/tests/policy/contrib/congress/wftca.yaml b/policyengine_us/tests/policy/contrib/congress/wftca.yaml index 0728a6620b4..c132349e12f 100644 --- a/policyengine_us/tests/policy/contrib/congress/wftca.yaml +++ b/policyengine_us/tests/policy/contrib/congress/wftca.yaml @@ -15,3 +15,26 @@ filing_status: JOINT output: bonus_guaranteed_deduction: 0 + +- name: WFTCA bonus raises the 2026 filing threshold for a single filer + period: 2026 + input: + gov.contrib.congress.wftca.bonus_guaranteed_deduction.amount.SINGLE: 2_000 + filing_status: SINGLE + age: 40 + taxable_interest_income: 18_099 + output: + # Section 6012(a)(1)(A)(i): $16,100 basic standard deduction plus the + # $2,000 increase to it; $18,099 is less than $18,100. + bonus_guaranteed_deduction: 2_000 + tax_unit_is_required_to_file: false + +- name: WFTCA bonus, 2026 single filer at the raised filing threshold + period: 2026 + input: + gov.contrib.congress.wftca.bonus_guaranteed_deduction.amount.SINGLE: 2_000 + filing_status: SINGLE + age: 40 + taxable_interest_income: 18_100 + output: + tax_unit_is_required_to_file: true diff --git a/policyengine_us/variables/gov/irs/tax_unit_is_required_to_file.py b/policyengine_us/variables/gov/irs/tax_unit_is_required_to_file.py index a7566a882e9..8fd7416bbb7 100644 --- a/policyengine_us/variables/gov/irs/tax_unit_is_required_to_file.py +++ b/policyengine_us/variables/gov/irs/tax_unit_is_required_to_file.py @@ -9,13 +9,26 @@ class tax_unit_is_required_to_file(Variable): documentation = """ Whether this tax unit is legally required to file a federal income tax return under IRC § 6012, based on gross income thresholds and filing - status. Under § 6012(c), gross income includes amounts excluded under - § 911: Form 2555 line 43 when Form 2555 leaf inputs are supplied, - otherwise the legacy foreign_earned_income_exclusion amount. + status. For 2018 to 2025, section 6012(f) compares gross income with the + standard deduction. In other years, section 6012(a)(1) compares it with + the basic standard deduction plus the additions for age, but not + blindness, and gross income equal to that amount requires a return. A + tax unit whose head or spouse can be claimed as a dependent keeps the + standard deduction test in every year. Under section 6012(c), both + tests count gross section 911 exclusions: Form 2555 line 43 when leaf + inputs are supplied, otherwise the nonnegative legacy + foreign_earned_income_exclusion amount. """ definition_period = YEAR reference = ( + # Section 6012(f) for 2018 to 2025, and 6012(a)(1)(A) and (B) in other + # years. "https://www.law.cornell.edu/uscode/text/26/6012", + # Chart A of the Form 1040 instructions for 2017, the last year + # section 6012(a)(1) governed. + "https://www.irs.gov/pub/irs-prior/i1040gi--2017.pdf#page=8", + # Separate filers with a zero exemption amount, and the IRS's $5. + "https://www.taxpayeradvocate.irs.gov/news/nta-blog/ntablog-irs-publication-error-may-have-caused-certain-married-taxpayers-filing-separately-to-fail-to-file-required-tax-returns/2019/12/", # A filer who can be claimed as a dependent: section 6012(a)(1)(C)(i) # and (f)(2)(D) (the section above), the section 63(c)(5) limited # standard deduction, and Publication 501, Table 2 (Chart B of the @@ -33,7 +46,8 @@ class tax_unit_is_required_to_file(Variable): def formula(tax_unit, period, parameters): # Section 6012(c): for the filing requirement, gross income is - # computed without regard to the section 911 exclusion. That is the + # computed without regard to the section 911 exclusion in every year, + # including the section 6012(a)(1) threshold from 2026. That is the # gross exclusion on Form 2555 line 43, not the worksheet line 2c # stacking amount, which is net of line 44 and worksheet line 2b and # includes the line 50 housing deduction. @@ -41,20 +55,67 @@ def formula(tax_unit, period, parameters): gross_income = add(tax_unit, period, ["irs_gross_income"]) + ( section_911_exclusion ) - p = parameters(period).gov.irs.income.exemption - exemption_amount = 0 if p.suspended else p.amount + p = parameters(period).gov.irs + exemption_amount = ( + 0 if p.income.exemption.suspended else p.income.exemption.amount + ) + filing_status = tax_unit("filing_status", period) + separate = filing_status == filing_status.possible_values.SEPARATE + head_can_be_claimed = tax_unit("head_is_dependent_elsewhere", period) + spouse_can_be_claimed = tax_unit("spouse_is_dependent_elsewhere", period) - # (a)(1)(A), (a)(1)(B) - filing_status = tax_unit("filing_status", period).decode_to_str() - separate = filing_status == "SEPARATE" - standard_deduction = tax_unit("standard_deduction", period) + # Section 6012(f), for 2018 to 2025: no return is required when gross + # income does not exceed the standard deduction, which includes the + # additional standard deduction for age and for blindness. A married + # individual filing separately must file with any gross income. threshold = where( separate, exemption_amount, - standard_deduction + exemption_amount, + tax_unit("standard_deduction", period) + exemption_amount, + ) + income_over_standard_deduction = gross_income > threshold + + # Section 6012(a)(1)(A) and (B), from 2026 (and before 2018): no return + # is required when gross income is less than the exemption amount for + # each spouse plus the basic standard deduction, raised by one + # additional standard deduction for each spouse aged 65 or older + # (section 63(f)(1)). Blindness does not raise it, and gross income + # equal to it requires a return. The WFTCA contributed reform's bonus + # guaranteed deduction is an increase to the basic standard deduction + # (zero unless that reform is on). + # + # A married individual filing separately has no exception. With an + # exemption amount of zero, (A) read literally reaches even an + # individual with no gross income; the National Taxpayer Advocate read + # the 2018 to 2025 rule that way, and the IRS set $5 as relief. As for + # 2018 to 2025, the model requires a return only for positive gross + # income (decision d1403). + aged_count = tax_unit("aged_head", period).astype(int) + tax_unit( + "aged_spouse", period + ).astype(int) + basic_threshold = ( + exemption_amount * tax_unit("head_spouse_count", period) + + tax_unit("basic_standard_deduction", period) + + tax_unit("bonus_guaranteed_deduction", period) + + aged_count * p.deductions.standard.aged_or_blind.amount[filing_status] + ) + income_at_least_basic_threshold = (gross_income > 0) & ( + gross_income >= where(separate, exemption_amount, basic_threshold) ) - income_over_exemption_amount = gross_income > threshold + # A tax unit whose head or spouse can be claimed as a dependent keeps + # the standard deduction test in every year. From 2026 it is + # (a)(1)(C)(i)(II), and it is what Chart B applies under (a)(1); the + # (A) threshold, which leaves out blindness and requires a return at + # equality, is not applied to these filers (decision d1395). + if p.income.filing_requirement.standard_deduction_rule.applies: + income_over_threshold = income_over_standard_deduction + else: + income_over_threshold = where( + head_can_be_claimed | spouse_can_be_claimed, + income_over_standard_deduction, + income_at_least_basic_threshold, + ) # A filer who can be claimed as a dependent on another return (an # individual described in section 63(c)(5)) must also file when their @@ -75,8 +136,6 @@ def formula(tax_unit, period, parameters): # is Table 2's gross income test, and earned income over the regular # amount plus the additional standard deduction always meets it. person = tax_unit.members - head_can_be_claimed = tax_unit("head_is_dependent_elsewhere", period) - spouse_can_be_claimed = tax_unit("spouse_is_dependent_elsewhere", period) can_be_claimed = ( person("is_tax_unit_head", period) & tax_unit.project(head_can_be_claimed) ) | ( @@ -94,12 +153,12 @@ def formula(tax_unit, period, parameters): for source in parameters(period).gov.irs.gross_income.sources: if source not in earned_income_sources: unearned_income += max_(0, add(person, period, [source])) - p_standard = parameters(period).gov.irs.deductions.standard + p_standard = p.deductions.standard aged_or_blind_count = person("is_irs_aged", period).astype(int) + person( "is_blind", period ).astype(int) additional_standard_deduction = aged_or_blind_count * tax_unit.project( - p_standard.aged_or_blind.amount[tax_unit("filing_status", period)] + p_standard.aged_or_blind.amount[filing_status] ) unearned_income_threshold = ( p_standard.dependent.amount + additional_standard_deduction @@ -108,4 +167,4 @@ def formula(tax_unit, period, parameters): can_be_claimed & (unearned_income > unearned_income_threshold) ) - return income_over_exemption_amount | unearned_income_over_threshold + return income_over_threshold | unearned_income_over_threshold