diff --git a/changelog.d/fix-ctc-ss-tax-filer-spouse.fixed.md b/changelog.d/fix-ctc-ss-tax-filer-spouse.fixed.md new file mode 100644 index 00000000000..b7b37243622 --- /dev/null +++ b/changelog.d/fix-ctc-ss-tax-filer-spouse.fixed.md @@ -0,0 +1 @@ +The refundable child tax credit's social security taxes (26 U.S.C. 24(d)(2)) and the Additional Medicare Tax (Form 8959) now count only the head's and spouse's wages and self-employment income, not a tax unit dependent's. The CRFB AGI surtax's expanded base now adds only the head's and spouse's contributions, exclusions and tax-exempt Social Security. diff --git a/policyengine_us/parameters/gov/contrib/crfb/surtax/increased_base/in_effect.yaml b/policyengine_us/parameters/gov/contrib/crfb/surtax/increased_base/in_effect.yaml index 2edbf44becf..842c1ab91d5 100644 --- a/policyengine_us/parameters/gov/contrib/crfb/surtax/increased_base/in_effect.yaml +++ b/policyengine_us/parameters/gov/contrib/crfb/surtax/increased_base/in_effect.yaml @@ -3,6 +3,9 @@ metadata: unit: bool period: year label: CRFB surtax expanded base in effect + reference: + - title: "Committee for a Responsible Federal Budget, Break Glass: A Plan for the Next Economic Shock, footnote 2" + href: https://www.crfb.org/papers/break-glass-plan-next-economic-shock values: 0000-01-01: false diff --git a/policyengine_us/parameters/gov/contrib/crfb/surtax/increased_base/sources.yaml b/policyengine_us/parameters/gov/contrib/crfb/surtax/increased_base/sources.yaml index 9bcb0de8221..a978ed74f69 100644 --- a/policyengine_us/parameters/gov/contrib/crfb/surtax/increased_base/sources.yaml +++ b/policyengine_us/parameters/gov/contrib/crfb/surtax/increased_base/sources.yaml @@ -1,6 +1,18 @@ description: Additional income sources for CRFB surtax expanded base beyond IRS gross income. values: 2010-01-01: + # CRFB's Break Glass plan (footnote 2) applies the surtax to "a broad + # definition of income that includes Adjusted Gross Income (AGI) plus" + # retirement contributions, HSA/MSA contributions, deductible student + # loan interest, Social Security not counted toward AGI, tax-free bond + # interest and employer-provided health insurance. It does not address + # dependents. The base starts from the return's AGI, so person-level + # sources are summed over the head and spouse only, the people whose AGI + # it is; that attribution is inferred. Tax-unit-level sources describe + # the head's and spouse's own return. Which items belong, and which + # variable stands for each, is a separate question + # (PolicyEngine/policyengine-us#9763); the footnote does not list the + # foreign earned income exclusion. # Retirement contributions (IRA, 401k, 403b) - traditional_ira_contributions - traditional_401k_contributions @@ -10,7 +22,7 @@ values: # Deductible student loan interest - student_loan_interest_ald # Social Security not counted toward AGI - - tax_exempt_social_security + - filer_tax_exempt_social_security # Foreign earnings exclusion (tax unit level, may need special handling) - foreign_earned_income_exclusion # Tax-free bond interest @@ -22,3 +34,6 @@ metadata: unit: list period: year label: CRFB surtax expanded base additional sources + reference: + - title: "Committee for a Responsible Federal Budget, Break Glass: A Plan for the Next Economic Shock, footnote 2" + href: https://www.crfb.org/papers/break-glass-plan-next-economic-shock diff --git a/policyengine_us/parameters/gov/irs/credits/ctc/refundable/social_security/add.yaml b/policyengine_us/parameters/gov/irs/credits/ctc/refundable/social_security/add.yaml index 6a191940a77..53123c3cadf 100644 --- a/policyengine_us/parameters/gov/irs/credits/ctc/refundable/social_security/add.yaml +++ b/policyengine_us/parameters/gov/irs/credits/ctc/refundable/social_security/add.yaml @@ -1,17 +1,19 @@ description: The IRS defines social security taxes under the Child Tax Credit to include the following variables. values: 2018-01-01: - # Person: + # Person (summed over the head and spouse only): - employee_social_security_tax - employee_medicare_tax + - self_employment_tax_ald_person + # Tax Unit (describes the head's and spouse's own return): - unreported_payroll_tax - # Tax Unit: - - self_employment_tax_ald - additional_medicare_tax metadata: unit: list period: year - label: Child Tax Credit added social security taxes + label: Child Tax Credit added social security taxes reference: - title: 26 U.S. Code ยง 24(d)(2)(A) href: https://www.law.cornell.edu/uscode/text/26/24#d_2_A + - title: 2024 Schedule 8812 (Form 1040), Part II-B, lines 21 to 23 + href: https://www.irs.gov/pub/irs-prior/f1040s8--2024.pdf#page=2 diff --git a/policyengine_us/reforms/biden/budget_2025/medicare_and_investment_tax_increase.py b/policyengine_us/reforms/biden/budget_2025/medicare_and_investment_tax_increase.py index 50bf3c0a703..853fa1f257e 100644 --- a/policyengine_us/reforms/biden/budget_2025/medicare_and_investment_tax_increase.py +++ b/policyengine_us/reforms/biden/budget_2025/medicare_and_investment_tax_increase.py @@ -19,7 +19,8 @@ def formula(tax_unit, period, parameters): "payroll_tax_gross_wages", "taxable_self_employment_income", ] - wages_plus_se = add(tax_unit, period, ELEMENTS) + # As in the baseline, only the head's and spouse's earnings. + wages_plus_se = tax_unit_non_dep_add(tax_unit, period, ELEMENTS) exclusion = amc.exclusion[tax_unit("filing_status", period)] base = max_(0, wages_plus_se - exclusion) base_tax = amc.rate * base diff --git a/policyengine_us/reforms/congress/wyden_smith/ctc_expansion.py b/policyengine_us/reforms/congress/wyden_smith/ctc_expansion.py index e4b770f5f5f..8d39284ee7e 100644 --- a/policyengine_us/reforms/congress/wyden_smith/ctc_expansion.py +++ b/policyengine_us/reforms/congress/wyden_smith/ctc_expansion.py @@ -57,22 +57,10 @@ def formula(tax_unit, period, parameters): relevant_earnings = earnings_over_threshold * phase_in_rate - # Compute "Social Security taxes" as defined in the US Code for the ACTC. - # This includes OASDI and Medicare payroll taxes, as well as half - # of self-employment taxes. - SS_ADD_VARIABLES = [ - # Person: - "employee_social_security_tax", - "employee_medicare_tax", - "unreported_payroll_tax", - # Tax unit: - "self_employment_tax_ald", - "additional_medicare_tax", - ] - SS_SUBTRACT_VARIABLES = ["excess_payroll_tax_withheld"] - social_security_tax = add(tax_unit, period, SS_ADD_VARIABLES) - add( - tax_unit, period, SS_SUBTRACT_VARIABLES - ) + # "Social Security taxes" as defined in 26 U.S.C. 24(d)(2) for the + # ACTC: the head's and spouse's OASDI and Medicare payroll taxes and + # half of their self-employment taxes, as in the baseline. + social_security_tax = tax_unit("ctc_social_security_tax", period) eitc = tax_unit("eitc", period) social_security_excess = max_(0, social_security_tax - eitc) qualifying_children = tax_unit("ctc_qualifying_children", period) diff --git a/policyengine_us/reforms/crfb/agi_surtax.py b/policyengine_us/reforms/crfb/agi_surtax.py index dc7d492de85..f10e1f63f72 100644 --- a/policyengine_us/reforms/crfb/agi_surtax.py +++ b/policyengine_us/reforms/crfb/agi_surtax.py @@ -15,7 +15,15 @@ def formula(tax_unit, period, parameters): agi = tax_unit("adjusted_gross_income", period) p = parameters(period).gov.contrib.crfb.surtax if p.increased_base.in_effect: - additional_sources = add(tax_unit, period, p.increased_base.sources) + # CRFB's Break Glass plan (footnote 2) defines the base as + # AGI plus other income. AGI is this return's, so the base + # adds the head's and spouse's own amounts; a tax unit + # dependent's income and deductions are on the dependent's + # own return. The plan does not address dependents, so this + # attribution is inferred. + additional_sources = tax_unit_non_dep_add( + tax_unit, period, p.increased_base.sources + ) agi = agi + additional_sources filing_status = tax_unit("filing_status", period) joint = filing_status == filing_status.possible_values.JOINT diff --git a/policyengine_us/tests/core/test_dependent_payroll_taxes_ctc_and_surtax_invariants.py b/policyengine_us/tests/core/test_dependent_payroll_taxes_ctc_and_surtax_invariants.py new file mode 100644 index 00000000000..3a71438bb9e --- /dev/null +++ b/policyengine_us/tests/core/test_dependent_payroll_taxes_ctc_and_surtax_invariants.py @@ -0,0 +1,454 @@ +"""Invariants for a tax unit dependent's payroll taxes and contributions. + +26 U.S.C. 24(d)(1)(B)(ii) lets a taxpayer with three or more qualifying +children figure the refundable child tax credit from "the taxpayer's social +security taxes" less the EITC, and 24(d)(2)(A) defines those as the taxes on +amounts the taxpayer received and on the taxpayer's self-employment income. +Schedule 8812 line 21 adds the spouse's on a joint return. The Additional +Medicare Tax among them comes from the return's own Form 8959, which counts +the wages of the taxpayer and, on a joint return, the spouse. A dependent's +wages are on the dependent's own Form W-2 and return. The CRFB AGI surtax's +expanded base adds amounts to this return's AGI, so it counts the head's and +spouse's own contributions, exclusions and deductions too. + +Each case runs every tax unit twice in one vectorized simulation: once with +the dependents' amounts as drawn and once with them set to zero. For every +tax unit: + +1. A dependent's wages, self-employment income, retirement contributions, + tax-exempt interest, Social Security benefits and health insurance + premiums never change `ctc_social_security_tax`, the Puerto Rico + equivalent, `additional_medicare_tax` or the CRFB surtax's additions to + AGI. +2. A dependent's wages, traditional IRA contributions, 401(k) and 403(b) + deferrals and Social Security benefits, none of which enters the filer's + AGI or deductions, never change the filer's AGI, refundable or + non-refundable CTC, federal income tax or CRFB surtax. A dependent's IRA + deduction is on the dependent's own return (#9801), so the surtax neither + subtracts it through AGI nor adds it back. +3. Differential: `ctc_social_security_tax`, the Puerto Rico equivalent, + `additional_medicare_tax` and the surtax equal an independent numpy + calculation over the head and spouse. + +Invariant 2 leaves out three kinds of dependent amount, which invariant 1 +checks with AGI and its taxable Social Security given as inputs: + +- A dependent's self-employment income still adds to the filer's qualified + business income deduction, which sums every member's `qbid_amount`. +- A dependent's tax-exempt interest counts toward the filer's EITC + investment income limit (#9635). +- Health insurance premiums a parent pays for a dependent may be the + parent's deductible medical expenses. +""" + +from functools import cache + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_us import Simulation +from policyengine_us.system import CountryTaxBenefitSystem + +TOLERANCE = 0.01 # dollars +YEARS = [2019, 2020, 2024, 2026] + +# The reform's in_effect parameter also applies its structural reform. +SURTAX_ON = { + "gov.contrib.crfb.surtax.in_effect": {"2000-01-01.2100-12-31": True}, + "gov.contrib.crfb.surtax.increased_base.in_effect": {"2000-01-01.2100-12-31": True}, +} + +# Dependent amounts that never enter the filer's AGI, deductions or credits. +OFF_RETURN_INPUTS = [ + "employment_income", + "traditional_ira_contributions_desired", + "traditional_401k_contributions_desired", + "traditional_403b_contributions_desired", + "social_security_dependents", +] +# Dependent amounts that reach the filer's return elsewhere: see the module +# docstring. +OTHER_INPUTS = [ + "self_employment_income", + "tax_exempt_interest_income", + "health_insurance_premiums", +] +PERSON_PAYROLL_TAXES = [ + "employee_social_security_tax", + "employee_medicare_tax", + "self_employment_tax_ald_person", +] +PERSON_SURTAX_SOURCES = [ + "traditional_ira_contributions", + "traditional_401k_contributions", + "traditional_403b_contributions", + "student_loan_interest_ald", + "tax_exempt_interest_income", + "health_insurance_premiums", +] + +money = st.integers(0, 60_000).map(float) + + +def maybe(strategy): + """Zero or a draw from the strategy.""" + return st.one_of(st.just(0.0), strategy) + + +@cache +def surtax_system(): + """The CRFB surtax with its expanded base, built once for every case.""" + return CountryTaxBenefitSystem(reform=SURTAX_ON) + + +@st.composite +def filer(draw): + return { + "age": draw(st.integers(25, 64)), + "employment_income": draw(maybe(st.integers(1, 300_000).map(float))), + "self_employment_income": draw(maybe(st.integers(-5_000, 60_000).map(float))), + # Over the EITC's investment income limit, the alternative refundable + # CTC formula decides more refunds. + "taxable_interest_income": draw(maybe(money)), + "social_security_disability": draw(maybe(money)), + "traditional_ira_contributions_desired": draw(maybe(st.just(5_000.0))), + "traditional_401k_contributions_desired": draw(maybe(money)), + "tax_exempt_interest_income": draw(maybe(money)), + "health_insurance_premiums": draw(maybe(st.integers(1, 9_000).map(float))), + } + + +@st.composite +def dependent(draw, *, every_amount): + # Under 18, so the model never treats a dependent as a spouse. + person = { + "age": draw(st.integers(0, 17)), + "employment_income": draw(maybe(money)), + "traditional_ira_contributions_desired": draw( + maybe(st.integers(1, 7_000).map(float)) + ), + "traditional_401k_contributions_desired": draw(maybe(money)), + "traditional_403b_contributions_desired": draw(maybe(money)), + "social_security_dependents": draw(maybe(st.integers(1, 15_000).map(float))), + } + if every_amount: + person["self_employment_income"] = draw( + maybe(st.integers(-5_000, 60_000).map(float)) + ) + person["tax_exempt_interest_income"] = draw(maybe(money)) + person["health_insurance_premiums"] = draw( + maybe(st.integers(1, 9_000).map(float)) + ) + return person + + +@st.composite +def tax_unit(draw, *, every_amount): + unit = { + "head": draw(filer()), + "spouse": draw(filer()) if draw(st.booleans()) else None, + "dependents": draw( + st.lists(dependent(every_amount=every_amount), min_size=1, max_size=5) + ), + "excess_payroll_tax_withheld": draw(maybe(st.integers(1, 2_000).map(float))), + } + if every_amount: + unit["adjusted_gross_income"] = draw(st.integers(0, 1_500_000).map(float)) + return unit + + +def without_dependent_amounts(unit, inputs): + dependents = [ + {k: (0.0 if k in inputs else v) for k, v in d.items()} + for d in unit["dependents"] + ] + return {**unit, "dependents": dependents} + + +def build(units, year): + """A situation with every tax unit, and which people are head or spouse.""" + people, tax_units, marital_units = {}, {}, {} + groups = {"households": {}, "spm_units": {}, "families": {}} + filer_flags, unit_index = [], [] + for i, unit in enumerate(units): + members = [] + filers = [("head", unit["head"])] + if unit["spouse"] is not None: + filers.append(("spouse", unit["spouse"])) + roles = filers + [ + (f"dependent{j}", d) for j, d in enumerate(unit["dependents"]) + ] + for role, values in roles: + name = f"{role}_{i}" + people[name] = {k: {year: v} for k, v in values.items()} + members.append(name) + filer_flags.append(not role.startswith("dependent")) + unit_index.append(i) + if role.startswith("dependent"): + marital_units[f"marital_unit_{name}"] = {"members": [name]} + marital_units[f"marital_unit_{i}"] = {"members": members[: len(filers)]} + tax_units[f"tax_unit_{i}"] = { + "members": members, + "excess_payroll_tax_withheld": {year: unit["excess_payroll_tax_withheld"]}, + } + if "adjusted_gross_income" in unit: + # AGI and the Social Security in it are given, so the filer's + # AGI cannot carry a dependent's deductions into the surtax. + taxable_social_security = 0.5 * sum( + values.get("social_security_disability", 0.0) for _, values in filers + ) + tax_units[f"tax_unit_{i}"].update( + adjusted_gross_income={year: unit["adjusted_gross_income"]}, + tax_unit_taxable_social_security={year: taxable_social_security}, + ) + groups["households"][f"household_{i}"] = { + "members": members, + "state_code": {year: "TX"}, + } + groups["spm_units"][f"spm_unit_{i}"] = {"members": members} + groups["families"][f"family_{i}"] = {"members": members} + situation = { + "people": people, + "tax_units": tax_units, + "marital_units": marital_units, + **groups, + } + return situation, np.array(filer_flags), np.array(unit_index) + + +def filer_sum(values, filers, unit_index): + """Sum a person-level array over each tax unit's head and spouse.""" + return np.bincount( + unit_index, weights=values * filers, minlength=unit_index.max() + 1 + ) + + +def run_twice(units, inputs, year, surtax=False): + """Calculate each unit with its dependents' amounts and without them.""" + zeroed = [without_dependent_amounts(unit, inputs) for unit in units] + situation, filers, unit_index = build(units + zeroed, year) + if surtax: + simulation = Simulation(situation=situation, tax_benefit_system=surtax_system()) + else: + simulation = Simulation(situation=situation) + return simulation, filers, unit_index + + +def calculate(simulation, variable, year): + return np.asarray(simulation.calculate(variable, year)) + + +def assert_unchanged(simulation, variables, year, n): + for variable in variables: + values = calculate(simulation, variable, year) + np.testing.assert_allclose( + values[:n], values[n:], atol=TOLERANCE, err_msg=variable + ) + + +def assert_payroll_taxes_match_reference(simulation, filers, unit_index, year): + person = { + v: calculate(simulation, v, year) + for v in [ + *PERSON_PAYROLL_TAXES, + "payroll_tax_gross_wages", + "taxable_self_employment_income", + ] + } + filing_status = simulation.calculate("filing_status", year).decode_to_str() + p = simulation.tax_benefit_system.parameters(year).gov.irs.payroll.medicare + threshold = p.additional.exclusion[np.asarray(filing_status)] + earnings = filer_sum( + person["payroll_tax_gross_wages"] + person["taxable_self_employment_income"], + filers, + unit_index, + ) + additional_medicare_tax = p.additional.rate * np.maximum(0, earnings - threshold) + np.testing.assert_allclose( + calculate(simulation, "additional_medicare_tax", year), + additional_medicare_tax, + atol=TOLERANCE, + ) + payroll_taxes = ( + sum(filer_sum(person[v], filers, unit_index) for v in PERSON_PAYROLL_TAXES) + + calculate(simulation, "unreported_payroll_tax", year) + + additional_medicare_tax + ) + np.testing.assert_allclose( + calculate(simulation, "pr_refundable_ctc_social_security_tax", year), + payroll_taxes, + atol=TOLERANCE, + ) + np.testing.assert_allclose( + calculate(simulation, "ctc_social_security_tax", year), + payroll_taxes - calculate(simulation, "excess_payroll_tax_withheld", year), + atol=TOLERANCE, + ) + + +def assert_surtax_matches_reference(simulation, filers, unit_index, year): + sources = sum( + filer_sum(calculate(simulation, v, year), filers, unit_index) + for v in PERSON_SURTAX_SOURCES + ) + social_security = filer_sum( + calculate(simulation, "social_security", year), filers, unit_index + ) + base = ( + calculate(simulation, "adjusted_gross_income", year) + + sources + + calculate(simulation, "health_savings_account_ald", year) + + calculate(simulation, "foreign_earned_income_exclusion", year) + + social_security + - calculate(simulation, "tax_unit_taxable_social_security", year) + ) + p = simulation.tax_benefit_system.parameters(year).gov.contrib.crfb.surtax + filing_status = np.asarray( + simulation.calculate("filing_status", year).decode_to_str() + ) + expected = np.where( + filing_status == "JOINT", p.rate.joint.calc(base), p.rate.single.calc(base) + ) + np.testing.assert_allclose( + calculate(simulation, "agi_surtax", year), expected, atol=TOLERANCE + ) + + +def check_off_return_amounts(units, year): + """Invariants 2 and 3; returns both simulations.""" + n = len(units) + baseline, filers, unit_index = run_twice(units, OFF_RETURN_INPUTS, year) + assert_unchanged( + baseline, + [ + "adjusted_gross_income", + "ctc_social_security_tax", + "pr_refundable_ctc_social_security_tax", + "additional_medicare_tax", + "ctc_phase_in", + "refundable_ctc", + "non_refundable_ctc", + "income_tax", + ], + year, + n, + ) + assert_payroll_taxes_match_reference(baseline, filers, unit_index, year) + reformed, filers, unit_index = run_twice( + units, OFF_RETURN_INPUTS, year, surtax=True + ) + assert_unchanged(reformed, ["agi_surtax"], year, n) + assert_surtax_matches_reference(reformed, filers, unit_index, year) + return baseline, reformed + + +def check_every_amount_with_agi_given(units, year): + """Invariants 1 and 3.""" + n = len(units) + simulation, filers, unit_index = run_twice( + units, OFF_RETURN_INPUTS + OTHER_INPUTS, year, surtax=True + ) + assert_unchanged( + simulation, + [ + "ctc_social_security_tax", + "pr_refundable_ctc_social_security_tax", + "additional_medicare_tax", + "agi_surtax", + ], + year, + n, + ) + assert_payroll_taxes_match_reference(simulation, filers, unit_index, year) + assert_surtax_matches_reference(simulation, filers, unit_index, year) + + +# Each case runs one or two vectorized simulations of up to 24 tax units. +# derandomize keeps CI runs reproducible. +SETTINGS = settings( + max_examples=5, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) + + +@SETTINGS +@given( + st.lists(tax_unit(every_amount=False), min_size=1, max_size=12), + st.sampled_from(YEARS), +) +def test_dependents_off_return_amounts_never_change_filer_ctc_tax_or_surtax( + units, year +): + check_off_return_amounts(units, year) + + +@SETTINGS +@given( + st.lists(tax_unit(every_amount=True), min_size=1, max_size=12), + st.sampled_from(YEARS), +) +def test_dependents_amounts_never_change_payroll_taxes_or_surtax_additions(units, year): + check_every_amount_with_agi_given(units, year) + + +def low_wage_family(married, dependent_wages): + """Three qualifying children, the EITC barred by interest income and + wages low enough that payroll taxes decide the refundable CTC.""" + head = {"age": 40, "employment_income": 1_000.0, "taxable_interest_income": 46e3} + spouse = {"age": 40, "employment_income": 3_000.0} if married else None + if not married: + head["employment_income"] = 4_000.0 + children = [ + {"age": 16, "employment_income": dependent_wages}, + {"age": 10}, + {"age": 8}, + ] + return { + "head": head, + "spouse": spouse, + "dependents": children, + "excess_payroll_tax_withheld": 0.0, + } + + +# The dependent's wages would push the parents over the joint Additional +# Medicare Tax threshold, their IRA deduction would lower the parents' AGI, +# and their contributions and benefits would add to the surtax base. +HIGH_INCOME_COUPLE = { + "head": {"age": 50, "employment_income": 150_000.0}, + "spouse": {"age": 48, "employment_income": 90_000.0}, + "dependents": [ + { + "age": 17, + "employment_income": 25_000.0, + "traditional_ira_contributions_desired": 5_000.0, + "traditional_401k_contributions_desired": 5_000.0, + "social_security_dependents": 6_000.0, + } + ], + "excess_payroll_tax_withheld": 0.0, +} + + +def test_fixed_households_where_the_alternative_formula_decides(): + units = [ + low_wage_family(married, wages) + for married in (False, True) + for wages in (8_000.0, 30_000.0) + ] + [HIGH_INCOME_COUPLE] + for year in YEARS: + baseline, reformed = check_off_return_amounts(units, year) + # The alternative formula decides each low-wage family's refund. + ss_tax = calculate(baseline, "ctc_social_security_tax", year) + eitc = calculate(baseline, "eitc", year) + earned = calculate(baseline, "ctc_phase_in_relevant_earnings", year) + refundable = calculate(baseline, "refundable_ctc", year) + assert (ss_tax[:4] - eitc[:4] > earned[:4] + 1).all() + np.testing.assert_allclose(refundable[:4], ss_tax[:4], atol=TOLERANCE) + # The dependent's wages would have raised the couple's tax. + assert calculate(baseline, "additional_medicare_tax", year)[4] == 0 + # The surtax applies to the couple. + assert calculate(reformed, "agi_surtax", year)[4] > 0 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_social_security_tax.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_social_security_tax.yaml index 00ca526300b..5b49a8fa388 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_social_security_tax.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_social_security_tax.yaml @@ -5,7 +5,7 @@ employee_social_security_tax: 0 employee_medicare_tax: 0 unreported_payroll_tax: 0 - self_employment_tax_ald: 0 + self_employment_tax_ald_person: 0 additional_medicare_tax: 0 # SS_SUBTRACT_VARIABLES excess_payroll_tax_withheld: 0 @@ -19,7 +19,7 @@ employee_social_security_tax: 100 employee_medicare_tax: 100 unreported_payroll_tax: 0 - self_employment_tax_ald: 50 + self_employment_tax_ald_person: 50 additional_medicare_tax: 25 # SS_SUBTRACT_VARIABLES excess_payroll_tax_withheld: 200 @@ -33,9 +33,43 @@ employee_social_security_tax: 100 employee_medicare_tax: 100 unreported_payroll_tax: 0 - self_employment_tax_ald: 50 + self_employment_tax_ald_person: 50 additional_medicare_tax: 25 # SS_SUBTRACT_VARIABLES excess_payroll_tax_withheld: 450 output: ctc_social_security_tax: -175 + +- name: Only the head's and spouse's payroll taxes count, not a dependent's + period: 2024 + input: + people: + head: + age: 40 + employee_social_security_tax: 300 + employee_medicare_tax: 70 + self_employment_tax_ald_person: 0 + spouse: + age: 40 + employee_social_security_tax: 100 + employee_medicare_tax: 30 + self_employment_tax_ald_person: 50 + dependent: + age: 16 + employee_social_security_tax: 400 + employee_medicare_tax: 90 + self_employment_tax_ald_person: 25 + tax_units: + tax_unit: + members: [head, spouse, dependent] + unreported_payroll_tax: 10 + additional_medicare_tax: 0 + excess_payroll_tax_withheld: 20 + marital_units: + marital_unit: + members: [head, spouse] + dependent_marital_unit: + members: [dependent] + output: + # 300 + 70 + 100 + 30 + 50 + 10 - 20; the dependent's $515 is left out. + ctc_social_security_tax: 540 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_social_security_tax_dependents.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_social_security_tax_dependents.yaml new file mode 100644 index 00000000000..13418144c55 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_social_security_tax_dependents.yaml @@ -0,0 +1,140 @@ +# 26 U.S.C. 24(d)(1)(B)(ii) and (d)(2)(A): a taxpayer with three or more +# qualifying children can have the refundable credit figured from the +# taxpayer's own social security taxes less the EITC (Schedule 8812 Part II-B). +# On a joint return the spouse's taxes count too ("If married filing jointly, +# include your spouse's amounts with yours", line 21). A tax unit dependent's +# payroll taxes are on the dependent's own Form W-2 and return, so they never +# count. +# +# Each household has $46,000 of taxable interest, which is more than the EITC's +# investment income limit, so the EITC is zero, and wages low enough that +# social security taxes exceed 15% of earned income over $2,500: the +# alternative formula decides the refund. One qualifying child, aged 16, has +# $8,000 of wages, with $612 (7.65%) of payroll tax that must not count. + +- name: Head of household with three children before 2021, a working child's payroll taxes do not count + period: 2020 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + employment_income: 4_000 + taxable_interest_income: 46_000 + child1: + age: 16 + employment_income: 8_000 + child2: + age: 10 + child3: + age: 8 + tax_units: + tax_unit: + members: [parent, child1, child2, child3] + households: + household: + members: [parent, child1, child2, child3] + state_code: TX + output: + filing_status: HEAD_OF_HOUSEHOLD + eitc: 0 + # AGI $50,000 less the $18,650 standard deduction is $31,350 of taxable + # income: 10% of $14,100 plus 12% of $17,250. + income_tax_before_credits: 3_480 + # 3 x $2,000. + ctc: 6_000 + # 15% x ($4,000 - $2,500). + ctc_phase_in_relevant_earnings: 225 + # The parent's 7.65% x $4,000 (lines 21 to 23); not the child's $612. + ctc_social_security_tax: 306 + # Line 26: the larger of $225 and $306 - $0 EITC. + ctc_phase_in: 306 + # Line 27: the smaller of line 17 ($4,200 = 3 x $1,400) and line 26. + refundable_ctc: 306 + +- name: Head of household with three children after 2025, a working child's payroll taxes do not count + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + employment_income: 4_000 + taxable_interest_income: 46_000 + child1: + age: 16 + employment_income: 8_000 + child2: + age: 10 + child3: + age: 8 + tax_units: + tax_unit: + members: [parent, child1, child2, child3] + households: + household: + members: [parent, child1, child2, child3] + state_code: TX + output: + filing_status: HEAD_OF_HOUSEHOLD + eitc: 0 + # AGI $50,000 less the $24,150 standard deduction is $25,850 of taxable + # income: 10% of $17,700 plus 12% of $8,150. + income_tax_before_credits: 2_748 + # 3 x $2,200. + ctc: 6_600 + ctc_phase_in_relevant_earnings: 225 + ctc_social_security_tax: 306 + ctc_phase_in: 306 + # The credit left after the $2,748 tax limit is $3,852, more than $306. + refundable_ctc: 306 + +- name: Joint return with three children, the spouse's payroll taxes count and a working child's do not + period: 2019 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 1_000 + taxable_interest_income: 46_000 + spouse: + age: 40 + employment_income: 3_000 + child1: + age: 16 + employment_income: 8_000 + child2: + age: 10 + child3: + age: 8 + tax_units: + tax_unit: + members: [head, spouse, child1, child2, child3] + marital_units: + marital_unit: + members: [head, spouse] + child1_marital_unit: + members: [child1] + child2_marital_unit: + members: [child2] + child3_marital_unit: + members: [child3] + households: + household: + members: [head, spouse, child1, child2, child3] + state_code: TX + output: + filing_status: JOINT + eitc: 0 + # AGI $50,000 less the $24,400 standard deduction is $25,600 of taxable + # income: 10% of $19,400 plus 12% of $6,200. + income_tax_before_credits: 2_684 + ctc: 6_000 + # 15% x ($4,000 - $2,500). + ctc_phase_in_relevant_earnings: 225 + # 7.65% x ($1,000 + $3,000). The head's own $76.50 alone would leave the + # earned income formula in charge. + ctc_social_security_tax: 306 + ctc_phase_in: 306 + refundable_ctc: 306 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/additional_medicare_tax.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/additional_medicare_tax.yaml index 47fd92327c2..5f9d5634d3e 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/additional_medicare_tax.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/additional_medicare_tax.yaml @@ -29,3 +29,50 @@ filing_status: SINGLE output: additional_medicare_tax: 450 # (250k - 200k) * 0.9% + +- name: Joint return counts both spouses' wages + period: 2024 + input: + people: + head: + age: 45 + employment_income: 200_000 + spouse: + age: 45 + employment_income: 100_000 + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + output: + filing_status: JOINT + additional_medicare_tax: 450 # (300k - 250k) * 0.9% + +- name: A dependent's wages are on the dependent's own Form 8959, not the parents' + period: 2024 + input: + people: + head: + age: 45 + employment_income: 140_000 + spouse: + age: 45 + employment_income: 100_000 + dependent: + age: 16 + employment_income: 20_000 + tax_units: + tax_unit: + members: [head, spouse, dependent] + marital_units: + marital_unit: + members: [head, spouse] + dependent_marital_unit: + members: [dependent] + output: + filing_status: JOINT + # The parents' $240,000 is under the $250,000 joint threshold; the + # dependent's $20,000 would otherwise add $90. + additional_medicare_tax: 0 diff --git a/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml b/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml index d34fd248f3f..f505b72c1c1 100644 --- a/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml +++ b/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml @@ -109,3 +109,145 @@ output: # Only AGI 80k used, below 100k threshold agi_surtax: 0 + +# The expanded base adds amounts to this return's AGI, so it counts the head's +# and spouse's own contributions, exclusions and deductions. A tax unit +# dependent's are on the dependent's own return. AGI is an input here, so these +# cases test the additions alone, whichever way the dependents' deductions +# enter AGI. + +- name: Expanded base leaves out a dependent's traditional IRA contribution + period: 2026 + reforms: policyengine_us.reforms.crfb.agi_surtax.agi_surtax_reform_object + input: + gov.contrib.crfb.surtax.in_effect: true + gov.contrib.crfb.surtax.increased_base.in_effect: true + people: + head: + age: 45 + spouse: + age: 45 + dependent: + age: 17 + employment_income: 10_000 + traditional_ira_contributions_desired: 5_000 + tax_units: + tax_unit: + members: [head, spouse, dependent] + adjusted_gross_income: 300_000 + marital_units: + marital_unit: + members: [head, spouse] + dependent_marital_unit: + members: [dependent] + output: + filing_status: JOINT + # The base stays at AGI, $300,000: (300k - 200k) * 1%. Adding the + # dependent's $5,000 would make it $1,050. + agi_surtax: 1_000 + +- name: Expanded base leaves out a dependent's Social Security benefits + period: 2026 + reforms: policyengine_us.reforms.crfb.agi_surtax.agi_surtax_reform_object + input: + gov.contrib.crfb.surtax.in_effect: true + gov.contrib.crfb.surtax.increased_base.in_effect: true + people: + head: + age: 45 + spouse: + age: 45 + dependent: + age: 10 + social_security_dependents: 10_000 + tax_units: + tax_unit: + members: [head, spouse, dependent] + adjusted_gross_income: 250_000 + marital_units: + marital_unit: + members: [head, spouse] + dependent_marital_unit: + members: [dependent] + output: + filing_status: JOINT + # The child's benefits are the child's income, not tax-exempt income of + # this return: (250k - 200k) * 1% = 500, not 600. + filer_tax_exempt_social_security: 0 + agi_surtax: 500 + +- name: Expanded base counts the spouse's items and leaves out the dependent's + period: 2026 + reforms: policyengine_us.reforms.crfb.agi_surtax.agi_surtax_reform_object + input: + gov.contrib.crfb.surtax.in_effect: true + gov.contrib.crfb.surtax.increased_base.in_effect: true + people: + head: + age: 45 + social_security_retirement: 20_000 + spouse: + age: 45 + employment_income: 50_000 + traditional_ira_contributions_desired: 5_000 + tax_exempt_interest_income: 2_000 + dependent: + age: 17 + employment_income: 20_000 + traditional_401k_contributions_desired: 3_000 + tax_exempt_interest_income: 1_000 + social_security_dependents: 4_000 + tax_units: + tax_unit: + members: [head, spouse, dependent] + adjusted_gross_income: 300_000 + tax_unit_taxable_social_security: 17_000 + marital_units: + marital_unit: + members: [head, spouse] + dependent_marital_unit: + members: [dependent] + output: + filing_status: JOINT + # The head's $20,000 of benefits less the $17,000 (85%) in AGI. + filer_tax_exempt_social_security: 3_000 + # 300k + 5k + 2k + 3k = 310k: (310k - 200k) * 1% = 1,100. The dependent's + # $3,000, $1,000 and $4,000 would make it $1,180. + agi_surtax: 1_100 + +# Here AGI is calculated. The dependent's IRA deduction is on the dependent's +# own return, so it is neither subtracted from the filer's AGI nor added back +# to the base (PolicyEngine/policyengine-us#9801 keeps it off the filer's AGI). + +- name: Expanded base with AGI calculated leaves a dependent's IRA contribution out of both AGI and the additions + period: 2024 + reforms: policyengine_us.reforms.crfb.agi_surtax.agi_surtax_reform_object + input: + gov.contrib.crfb.surtax.in_effect: true + gov.contrib.crfb.surtax.increased_base.in_effect: true + people: + head: + age: 45 + employment_income: 150_000 + dependent: + age: 17 + employment_income: 10_000 + traditional_ira_contributions_desired: 5_000 + tax_units: + tax_unit: + members: [head, dependent] + marital_units: + marital_unit: + members: [head] + dependent_marital_unit: + members: [dependent] + output: + filing_status: HEAD_OF_HOUSEHOLD + is_tax_unit_dependent: [false, true] + traditional_ira_contributions: [0, 5_000] + # Only the head's $150,000 of wages. Subtracting the dependent's $5,000 + # IRA deduction would make it $145,000. + adjusted_gross_income: 150_000 + # (150k - 100k) * 1% = 500. With the deduction in AGI and no add-back, + # the base would be $145,000 and the surtax $450. + agi_surtax: 500 diff --git a/policyengine_us/variables/gov/irs/credits/ctc/refundable/ctc_social_security_tax.py b/policyengine_us/variables/gov/irs/credits/ctc/refundable/ctc_social_security_tax.py index c54bb7ab002..5cb3eac362d 100644 --- a/policyengine_us/variables/gov/irs/credits/ctc/refundable/ctc_social_security_tax.py +++ b/policyengine_us/variables/gov/irs/credits/ctc/refundable/ctc_social_security_tax.py @@ -7,10 +7,22 @@ class ctc_social_security_tax(Variable): label = "Refundable Child Tax Credit Social Security Tax" unit = USD documentation = ( - "Social Security taxes considered in the Child Tax Credit calculation" + "Social Security taxes considered in the Child Tax Credit calculation: " + "the head's and, on a joint return, the spouse's (Schedule 8812 " + "Part II-B, lines 21 to 23, less the excess withheld on Schedule 3). " + "Section 24(d)(2)(A) counts the taxpayer's own payroll taxes, so a " + "tax unit dependent's payroll taxes, which are figured on the " + "dependent's own wages and own return, are left out." ) definition_period = YEAR - reference = "https://www.law.cornell.edu/uscode/text/26/24#d_2" + reference = ( + "https://www.law.cornell.edu/uscode/text/26/24#d_2", + "https://www.irs.gov/pub/irs-prior/f1040s8--2024.pdf#page=2", + "https://www.irs.gov/pub/irs-prior/i1040s8--2024.pdf#page=9", + ) - adds = "gov.irs.credits.ctc.refundable.social_security.add" - subtracts = "gov.irs.credits.ctc.refundable.social_security.subtract" + def formula(tax_unit, period, parameters): + p = parameters(period).gov.irs.credits.ctc.refundable.social_security + added = tax_unit_non_dep_add(tax_unit, period, p.add) + subtracted = tax_unit_non_dep_add(tax_unit, period, p.subtract) + return added - subtracted diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/social_security/filer_tax_exempt_social_security.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/social_security/filer_tax_exempt_social_security.py new file mode 100644 index 00000000000..1bba32c881d --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/social_security/filer_tax_exempt_social_security.py @@ -0,0 +1,20 @@ +from policyengine_us.model_api import * + + +class filer_tax_exempt_social_security(Variable): + value_type = float + entity = TaxUnit + label = "Filer's tax-exempt Social Security" + unit = USD + documentation = ( + "Social Security benefits of the head and spouse that section 86 " + "leaves out of this return's AGI. A tax unit dependent's benefits are " + "the dependent's own income, as in " + "tax_unit_social_security_for_taxability. Unlike " + "tax_exempt_social_security, this leaves dependents' benefits out." + ) + definition_period = YEAR + reference = "https://www.law.cornell.edu/uscode/text/26/86" + + adds = ["tax_unit_social_security_for_taxability"] + subtracts = ["tax_unit_taxable_social_security"] diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/additional_medicare_tax.py b/policyengine_us/variables/gov/irs/tax/federal_income/additional_medicare_tax.py index 49fc65c7bd6..dd30d2bda54 100644 --- a/policyengine_us/variables/gov/irs/tax/federal_income/additional_medicare_tax.py +++ b/policyengine_us/variables/gov/irs/tax/federal_income/additional_medicare_tax.py @@ -7,13 +7,23 @@ class additional_medicare_tax(Variable): definition_period = YEAR label = "Additional Medicare Tax" unit = USD - documentation = "Additional Medicare Tax from Form 8959 (included in payrolltax)" + documentation = ( + "Additional Medicare Tax from Form 8959 (included in payrolltax), on " + "the Medicare wages and self-employment income of the head and, on a " + "joint return, the spouse. A tax unit dependent's wages are reported " + "on the dependent's own Form W-2 and own return, so they are left out." + ) + reference = ( + "https://www.law.cornell.edu/uscode/text/26/3101#b_2", + "https://www.law.cornell.edu/uscode/text/26/1401#b_2", + "https://www.irs.gov/pub/irs-prior/i8959--2024.pdf#page=3", + ) def formula(tax_unit, period, parameters): amc = parameters(period).gov.irs.payroll.medicare.additional # Wage and self-employment income are taxed the same. ELEMENTS = ["payroll_tax_gross_wages", "taxable_self_employment_income"] - wages_plus_se = add(tax_unit, period, ELEMENTS) + wages_plus_se = tax_unit_non_dep_add(tax_unit, period, ELEMENTS) exclusion = amc.exclusion[tax_unit("filing_status", period)] base = max_(0, wages_plus_se - exclusion) return amc.rate * base diff --git a/policyengine_us/variables/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc_social_security_tax.py b/policyengine_us/variables/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc_social_security_tax.py index 105f3bc114e..e809c2aac0a 100644 --- a/policyengine_us/variables/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc_social_security_tax.py +++ b/policyengine_us/variables/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc_social_security_tax.py @@ -7,12 +7,20 @@ class pr_refundable_ctc_social_security_tax(Variable): label = ( "Puerto Rico social security and medicare taxes for refundable Child Tax Credit" ) + documentation = ( + "The head's and, on a joint return, the spouse's social security and " + "Medicare taxes. Section 24(d)(2)(A) counts the taxpayer's own " + "payroll taxes, so a tax unit dependent's are left out." + ) unit = USD definition_period = YEAR reference = ( "https://www.irs.gov/pub/irs-pdf/f1040s8.pdf", "https://www.law.cornell.edu/uscode/text/26/24#h_4_A", + "https://www.law.cornell.edu/uscode/text/26/24#d_2", ) # line 23 - adds = "gov.irs.credits.ctc.refundable.social_security.add" + def formula(tax_unit, period, parameters): + p = parameters(period).gov.irs.credits.ctc.refundable.social_security + return tax_unit_non_dep_add(tax_unit, period, p.add)