From 8cf97a7c22ecfc83c1dbcb7ea3110778f02a7ffb Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 5 Oct 2026 15:34:04 -0400 Subject: [PATCH 1/6] Count only the head's and spouse's payroll taxes in the refundable CTC 26 U.S.C. 24(d)(2)(A) defines the refundable credit's social security taxes as the taxpayer's own, and Schedule 8812 line 21 adds the spouse's on a joint return. ctc_social_security_tax summed every tax unit member, dependents included. It now sums the person-level taxes over the head and spouse with tax_unit_non_dep_add, and takes the deductible part of self-employment tax per person, so it is right whether or not #9801 lands first. The Additional Medicare Tax it counts comes from the return's own Form 8959, which counts the head's and spouse's wages only, so additional_medicare_tax (and the Biden budget reform's copy of it) now leaves dependents' wages out. The Puerto Rico refundable CTC and the Wyden-Smith reform used the same sum; the latter now reads ctc_social_security_tax. Co-Authored-By: Claude Opus 5.5 --- .../ctc/refundable/social_security/add.yaml | 10 +- .../medicare_and_investment_tax_increase.py | 3 +- .../congress/wyden_smith/ctc_expansion.py | 20 +-- .../refundable/ctc_social_security_tax.yaml | 40 ++++- .../ctc_social_security_tax_dependents.yaml | 140 ++++++++++++++++++ .../additional_medicare_tax.yaml | 47 ++++++ .../ctc/refundable/ctc_social_security_tax.py | 20 ++- .../federal_income/additional_medicare_tax.py | 14 +- .../pr_refundable_ctc_social_security_tax.py | 10 +- 9 files changed, 273 insertions(+), 31 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_social_security_tax_dependents.yaml 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/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/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/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) From 027eba5d3316c56c0f0fc60b7f83a3be60c7f4de Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 5 Oct 2026 15:36:13 -0400 Subject: [PATCH 2/6] Add only the head's and spouse's amounts to the CRFB surtax's expanded base The CRFB AGI surtax's expanded base adds retirement contributions, tax-exempt interest, health insurance premiums, the HSA and student loan deductions, the foreign earned income exclusion and tax-exempt Social Security to AGI. It summed every tax unit member, so a dependent's 401(k) deferrals, tax-exempt interest and IRA contributions were added to a return whose AGI never held them. tax_exempt_social_security also carried a dependent's whole benefit, since only the head's and spouse's benefits enter the section 86 calculation. The base now adds the person-level sources over the head and spouse with tax_unit_non_dep_add, and takes Social Security from a new filer_tax_exempt_social_security: the head's and spouse's benefits less the taxable part. The YAML cases give AGI as an input, so they test the additions alone, before or after #9801. Co-Authored-By: Claude Opus 5.5 --- .../crfb/surtax/increased_base/sources.yaml | 5 +- policyengine_us/reforms/crfb/agi_surtax.py | 7 +- .../tests/policy/contrib/crfb/agi_surtax.yaml | 105 ++++++++++++++++++ .../filer_tax_exempt_social_security.py | 20 ++++ 4 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/social_security/filer_tax_exempt_social_security.py 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..1f3859200d1 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,9 @@ description: Additional income sources for CRFB surtax expanded base beyond IRS gross income. values: 2010-01-01: + # Person-level sources are summed over the head and spouse only, the + # people whose AGI the base expands; tax-unit-level sources describe the + # head's and spouse's own return. # Retirement contributions (IRA, 401k, 403b) - traditional_ira_contributions - traditional_401k_contributions @@ -10,7 +13,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 diff --git a/policyengine_us/reforms/crfb/agi_surtax.py b/policyengine_us/reforms/crfb/agi_surtax.py index dc7d492de85..ca41eda01ba 100644 --- a/policyengine_us/reforms/crfb/agi_surtax.py +++ b/policyengine_us/reforms/crfb/agi_surtax.py @@ -15,7 +15,12 @@ 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) + # The base expands this return's AGI, so it adds the head's + # and spouse's own amounts. A tax unit dependent's income and + # deductions are on the dependent's own return. + 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/policy/contrib/crfb/agi_surtax.yaml b/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml index d34fd248f3f..d5bab15bc88 100644 --- a/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml +++ b/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml @@ -109,3 +109,108 @@ 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 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"] From 34e3dc974d117b393dd3b002d41e2b611f75d91c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 5 Oct 2026 17:14:14 -0400 Subject: [PATCH 3/6] Test that dependents' payroll taxes and contributions stay off the filer's return Hypothesis draws batches of tax units in 2019, 2020, 2024 and 2026 and runs each with and without its dependents' amounts in one simulation: - A dependent's wages, 401(k) and 403(b) deferrals and Social Security never change the filer's AGI, CTC, income tax or CRFB surtax. - With AGI given, no dependent amount changes the CTC's payroll taxes, the Additional Medicare Tax or the surtax's additions. - Each of these equals an independent numpy sum over the head and spouse. A fixed-household case checks that the alternative refundable formula decides the refund. All three tests fail on main. Co-Authored-By: Claude Opus 5.5 --- .../fix-ctc-ss-tax-filer-spouse.fixed.md | 1 + ...payroll_taxes_ctc_and_surtax_invariants.py | 450 ++++++++++++++++++ 2 files changed, 451 insertions(+) create mode 100644 changelog.d/fix-ctc-ss-tax-filer-spouse.fixed.md create mode 100644 policyengine_us/tests/core/test_dependent_payroll_taxes_ctc_and_surtax_invariants.py 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/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..a6e6adc0637 --- /dev/null +++ b/policyengine_us/tests/core/test_dependent_payroll_taxes_ctc_and_surtax_invariants.py @@ -0,0 +1,450 @@ +"""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, 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. +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 two kinds of dependent amount, which invariant 1 +checks with AGI and its taxable Social Security given as inputs: + +- A dependent's IRA contribution and the deductible part of their + self-employment tax are subtracted from the filer's AGI and Social Security + MAGI until PolicyEngine/policyengine-us#9801 lands. +- A dependent's tax-exempt interest counts toward the filer's EITC + investment income limit (#9635). +""" + +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_401k_contributions_desired", + "traditional_403b_contributions_desired", + "social_security_dependents", +] +# Dependent amounts that reach the filer's return elsewhere: see the module +# docstring. Premiums a parent pays may be deductible medical expenses. +OTHER_INPUTS = [ + "self_employment_income", + "traditional_ira_contributions_desired", + "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_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["traditional_ira_contributions_desired"] = draw( + maybe(st.integers(1, 7_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, and their deferral 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_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 From 795b0f166b9dd5730e9a68771de0ae9283ecf682 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 06:28:10 -0400 Subject: [PATCH 4/6] Test the CRFB surtax with AGI calculated, now that #9801 is in A 2024 head of household earns $150,000; a dependent earns $10,000 and contributes $5,000 to an IRA. The dependent's IRA deduction is on the dependent's own return, so AGI is $150,000 and the expanded base adds nothing for it: the surtax is $500. The case fails at the old head of this branch (AGI $145,000, surtax $450), on main before #9801 (AGI $145,000) and on main without this branch (surtax $550). With #9801 in, the property test's full-pipeline invariant also covers a dependent's IRA contribution. Self-employment income stays out: the qualified business income deduction still sums every member's qbid_amount. Co-Authored-By: Claude Opus 5.5 --- ...payroll_taxes_ctc_and_surtax_invariants.py | 34 +++++++++-------- .../tests/policy/contrib/crfb/agi_surtax.yaml | 37 +++++++++++++++++++ 2 files changed, 56 insertions(+), 15 deletions(-) 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 index a6e6adc0637..3a71438bb9e 100644 --- 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 @@ -20,22 +20,25 @@ 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, 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. +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 two kinds of dependent amount, which invariant 1 +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 IRA contribution and the deductible part of their - self-employment tax are subtracted from the filer's AGI and Social Security - MAGI until PolicyEngine/policyengine-us#9801 lands. +- 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 @@ -59,15 +62,15 @@ # 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. Premiums a parent pays may be deductible medical expenses. +# docstring. OTHER_INPUTS = [ "self_employment_income", - "traditional_ira_contributions_desired", "tax_exempt_interest_income", "health_insurance_premiums", ] @@ -122,6 +125,9 @@ def dependent(draw, *, every_amount): 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))), @@ -130,9 +136,6 @@ def dependent(draw, *, every_amount): person["self_employment_income"] = draw( maybe(st.integers(-5_000, 60_000).map(float)) ) - person["traditional_ira_contributions_desired"] = draw( - maybe(st.integers(1, 7_000).map(float)) - ) person["tax_exempt_interest_income"] = draw(maybe(money)) person["health_insurance_premiums"] = draw( maybe(st.integers(1, 9_000).map(float)) @@ -412,8 +415,8 @@ def low_wage_family(married, dependent_wages): # The dependent's wages would push the parents over the joint Additional -# Medicare Tax threshold, and their deferral and benefits would add to the -# surtax base. +# 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}, @@ -421,6 +424,7 @@ def low_wage_family(married, dependent_wages): { "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, } diff --git a/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml b/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml index d5bab15bc88..7147261cb06 100644 --- a/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml +++ b/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml @@ -214,3 +214,40 @@ # 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 subtracted from neither 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 From d2b6e053b9e15ecf2c6a53e4926c12737bf130dc Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 06:28:44 -0400 Subject: [PATCH 5/6] Cite CRFB's Break Glass plan for the surtax's expanded base Footnote 2 of "Break Glass: A Plan for the Next Economic Shock" defines the individual surtax base as 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. The plan does not address dependents; the comments now say the head-and-spouse attribution is inferred from the base starting at the return's AGI. Co-Authored-By: Claude Opus 5.5 --- .../crfb/surtax/increased_base/in_effect.yaml | 3 +++ .../crfb/surtax/increased_base/sources.yaml | 15 ++++++++++++--- policyengine_us/reforms/crfb/agi_surtax.py | 9 ++++++--- 3 files changed, 21 insertions(+), 6 deletions(-) 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 1f3859200d1..bde53d4c3a7 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,9 +1,15 @@ description: Additional income sources for CRFB surtax expanded base beyond IRS gross income. values: 2010-01-01: - # Person-level sources are summed over the head and spouse only, the - # people whose AGI the base expands; tax-unit-level sources describe the - # head's and spouse's own return. + # 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. # Retirement contributions (IRA, 401k, 403b) - traditional_ira_contributions - traditional_401k_contributions @@ -25,3 +31,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/reforms/crfb/agi_surtax.py b/policyengine_us/reforms/crfb/agi_surtax.py index ca41eda01ba..f10e1f63f72 100644 --- a/policyengine_us/reforms/crfb/agi_surtax.py +++ b/policyengine_us/reforms/crfb/agi_surtax.py @@ -15,9 +15,12 @@ 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: - # The base expands this return's AGI, so it adds the head's - # and spouse's own amounts. A tax unit dependent's income and - # deductions are on the dependent's own return. + # 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 ) From c6914e7d39979f520e879e1fbd28afa9176bb163 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 08:57:57 -0400 Subject: [PATCH 6/6] Point the surtax source list at #9763 for which items belong The list includes the foreign earned income exclusion, which footnote 2 of CRFB's Break Glass plan does not name, and #9763 tracks how the health insurance and retirement entries map to variables. Co-Authored-By: Claude Opus 5.5 --- .../gov/contrib/crfb/surtax/increased_base/sources.yaml | 5 ++++- policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) 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 bde53d4c3a7..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 @@ -9,7 +9,10 @@ values: # 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. + # 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 diff --git a/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml b/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml index 7147261cb06..f505b72c1c1 100644 --- a/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml +++ b/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml @@ -216,7 +216,7 @@ agi_surtax: 1_100 # Here AGI is calculated. The dependent's IRA deduction is on the dependent's -# own return, so it is subtracted from neither the filer's AGI nor added back +# 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