From 1952b5ecb739bf7c61c7639bf9b71dee80fc52db Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 08:39:49 -0400 Subject: [PATCH 01/12] Deny the refundable CTC to filers electing the section 911 exclusion 26 U.S.C. 24(d)(3) says the refundable portion of the Child Tax Credit "shall not apply to any taxpayer for any taxable year if such taxpayer elects to exclude any amount from gross income under section 911 for such taxable year". refundable_ctc never read foreign_earned_income_exclusion, so Form 2555 filers got the additional child tax credit. - Add gov.irs.credits.ctc.refundable.foreign_earned_income_exclusion_bar_applies (false before 2015, true from 2015: Pub. L. 114-27 section 807 applies to taxable years beginning after 2014). - refundable_ctc is zero when the exclusion is positive and the bar applies. The fully refundable year (2021) is unchanged: section 24(i)(1)(A) switched off subsection (d) for filers living in the United States, which the model assumes. - Apply the same bar in the Wyden-Smith reform's copy of refundable_ctc (H.R. 7024 amends section 24(h) and leaves 24(d)(3) in place). - Add YAML unit, household and reform tests, and Hypothesis property tests (hypothesis joins the dev extras). Co-Authored-By: Claude Opus 5.5 --- ...e-foreign-earned-income-exclusion.fixed.md | 1 + ...n_earned_income_exclusion_bar_applies.yaml | 18 ++ .../congress/wyden_smith/ctc_expansion.py | 13 +- .../gov/irs/credits/ctc/integration.yaml | 73 +++++++ .../ctc/refundable/refundable_ctc.yaml | 100 ++++++++++ .../tests/policy/reform/ctc_expansion.yaml | 17 ++ ...est_ctc_foreign_earned_income_exclusion.py | 182 ++++++++++++++++++ .../credits/ctc/refundable/refundable_ctc.py | 27 ++- pyproject.toml | 1 + uv.lock | 90 +++++++++ 10 files changed, 519 insertions(+), 3 deletions(-) create mode 100644 changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md create mode 100644 policyengine_us/parameters/gov/irs/credits/ctc/refundable/foreign_earned_income_exclusion_bar_applies.yaml create mode 100644 policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py diff --git a/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md b/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md new file mode 100644 index 00000000000..7850caf502d --- /dev/null +++ b/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md @@ -0,0 +1 @@ +Deny the refundable Child Tax Credit to filers who elect to exclude foreign earned income or housing amounts under section 911 (26 U.S.C. 24(d)(3)), for tax years from 2015, including under the Wyden-Smith CTC expansion reform. diff --git a/policyengine_us/parameters/gov/irs/credits/ctc/refundable/foreign_earned_income_exclusion_bar_applies.yaml b/policyengine_us/parameters/gov/irs/credits/ctc/refundable/foreign_earned_income_exclusion_bar_applies.yaml new file mode 100644 index 00000000000..15b1c7a0fb5 --- /dev/null +++ b/policyengine_us/parameters/gov/irs/credits/ctc/refundable/foreign_earned_income_exclusion_bar_applies.yaml @@ -0,0 +1,18 @@ +description: The IRS denies the refundable Child Tax Credit to filers who elect to exclude any amount from gross income under section 911 (the foreign earned income and housing exclusions) if this is true. +values: + 2013-01-01: false + 2015-01-01: true +metadata: + unit: bool + period: year + label: Refundable Child Tax Credit denied to section 911 exclusion filers + reference: + - title: 26 U.S. Code § 24(d)(3) + href: https://www.law.cornell.edu/uscode/text/26/24#d_3 + # Section 807 of the Trade Preferences Extension Act of 2015 added this + # rule as section 24(d)(5) for taxable years beginning after 2014; Pub. L. + # 115-141 redesignated it as paragraph (3) in 2018. + - title: Public Law 114-27, § 807 + href: https://www.govinfo.gov/content/pkg/PLAW-114publ27/pdf/PLAW-114publ27.pdf#page=58 + - title: 2025 Instructions for Schedule 8812, Part II-A + href: https://www.irs.gov/pub/irs-pdf/i1040s8.pdf#page=3 diff --git a/policyengine_us/reforms/congress/wyden_smith/ctc_expansion.py b/policyengine_us/reforms/congress/wyden_smith/ctc_expansion.py index e4b770f5f5f..a3d024cd7d5 100644 --- a/policyengine_us/reforms/congress/wyden_smith/ctc_expansion.py +++ b/policyengine_us/reforms/congress/wyden_smith/ctc_expansion.py @@ -86,8 +86,19 @@ def formula(tax_unit, period, parameters): ctc_capped_by_tax = min_(total_ctc, limiting_tax) ctc_capped_by_increased_tax = min_(total_ctc, limiting_tax + tax_increase) amount_ctc_would_increase = ctc_capped_by_increased_tax - ctc_capped_by_tax + refundable_amount = min_(maximum_refundable_ctc, amount_ctc_would_increase) - return min_(maximum_refundable_ctc, amount_ctc_would_increase) + # The bill's CTC changes amend section 24(h) and leave section + # 24(d)(3) in place: no refundable CTC for filers electing a + # section 911 exclusion. + elects_section_911_exclusion = ( + tax_unit("foreign_earned_income_exclusion", period) > 0 + ) + barred = ( + elects_section_911_exclusion + & p_ctc.refundable.foreign_earned_income_exclusion_bar_applies + ) + return where(barred, 0, refundable_amount) class reform(Reform): def apply(self): diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/integration.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/integration.yaml index 181319ef027..a5047600abf 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/integration.yaml @@ -486,3 +486,76 @@ output: ctc: 3_500 # 3000 for 10-year-old + 500 for 18-year-old (ODC) ctc_adult_individual_maximum: [0, 0, 500] # ODC for 18-year-old under ARPA + +- name: Head of household with two children and a section 911 exclusion gets no additional child tax credit in 2025. + period: 2025 + absolute_error_margin: 0 + input: + people: + parent: + age: 40 + # Wages included in gross income; the excluded foreign wages are + # reported separately below. + employment_income: 60_000 + child1: + age: 5 + child2: + age: 8 + tax_units: + tax_unit: + members: [parent, child1, child2] + # Form 2555 exclusion. + foreign_earned_income_exclusion: 40_000 + households: + household: + members: [parent, child1, child2] + state_code: TX + output: + filing_status: HEAD_OF_HOUSEHOLD + adjusted_gross_income: 60_000 + # $60,000 less the $23,625 head of household standard deduction. + taxable_income: 36_375 + # 10% of $17,000 plus 12% of the $19,375 above it. + income_tax_before_credits: 4_025 + # Schedule 8812 line 12: 2 x $2,200. + ctc: 4_400 + # Line 13 (Credit Limit Worksheet A): $4,025 of tax, no other credits. + ctc_limiting_tax_liability: 4_025 + # Part II-A: "If you file Form 2555, you cannot claim the additional + # child tax credit." Without Form 2555, line 27 would be $375. + refundable_ctc: 0 + # Line 14 allows $4,025 of the $4,400 against tax. + non_refundable_ctc: 4_400 + income_tax_before_refundable_credits: 0 + income_tax: 0 + +- name: The same household without a section 911 exclusion gets a $375 additional child tax credit in 2025. + period: 2025 + absolute_error_margin: 0 + input: + people: + parent: + age: 40 + employment_income: 60_000 + child1: + age: 5 + child2: + age: 8 + tax_units: + tax_unit: + members: [parent, child1, child2] + foreign_earned_income_exclusion: 0 + households: + household: + members: [parent, child1, child2] + state_code: TX + output: + income_tax_before_credits: 4_025 + ctc: 4_400 + # Part II-A: line 16a = $4,400 - $4,025 = $375; line 16b = 2 x $1,700 + # = $3,400; line 17 = $375; line 20 = 15% x ($60,000 - $2,500) = $8,625; + # line 27 = smaller of lines 17 and 20 = $375. + refundable_ctc: 375 + non_refundable_ctc: 4_025 + income_tax_before_refundable_credits: 0 + income_tax: -375 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/refundable_ctc.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/refundable_ctc.yaml index 268f999c09d..1c690eab327 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/refundable_ctc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/refundable_ctc.yaml @@ -81,3 +81,103 @@ ctc_limiting_tax_liability: 4_000 output: refundable_ctc: 0 + +# Section 24(d)(3): no refundable CTC for a filer who elects to exclude any +# amount from gross income under section 911 (2025 Schedule 8812 +# instructions, Part II-A: "If you file Form 2555, you cannot claim the +# additional child tax credit."). +- name: Section 911 exclusion denies the refundable credit + period: 2025 + input: + ctc_refundable_maximum: 3_400 + ctc: 4_400 + ctc_phase_out: 0 + ctc_phase_in: 10_000 + ctc_limiting_tax_liability: 0 + foreign_earned_income_exclusion: 1 + output: + # Without the exclusion: min(3,400, min(4,400, 0 + 10,000) - 0) = 3,400. + refundable_ctc: 0 + +- name: Section 911 exclusion denies the refundable credit from social security taxes + period: 2025 + input: + # Three or more children: the phase-in amount reflects social security + # taxes less the EITC (section 24(d)(1)(B)(ii)), which section 24(d)(3) + # also switches off. + ctc_refundable_maximum: 5_100 + ctc: 6_600 + ctc_phase_out: 0 + ctc_phase_in: 4_000 + ctc_limiting_tax_liability: 1_000 + foreign_earned_income_exclusion: 30_000 + output: + refundable_ctc: 0 + +- name: No section 911 exclusion keeps the refundable credit + period: 2025 + input: + ctc_refundable_maximum: 5_100 + ctc: 6_600 + ctc_phase_out: 0 + ctc_phase_in: 4_000 + ctc_limiting_tax_liability: 1_000 + foreign_earned_income_exclusion: 0 + output: + # min(5,100, min(6,600, 1,000 + 4,000) - min(6,600, 1,000)) = 4,000. + refundable_ctc: 4_000 + +- name: Section 911 exclusion bar can be switched off + period: 2025 + input: + gov.irs.credits.ctc.refundable.foreign_earned_income_exclusion_bar_applies: false + ctc_refundable_maximum: 5_100 + ctc: 6_600 + ctc_phase_out: 0 + ctc_phase_in: 4_000 + ctc_limiting_tax_liability: 1_000 + foreign_earned_income_exclusion: 30_000 + output: + refundable_ctc: 4_000 + +- name: Section 911 exclusion did not deny the refundable credit before 2015 + period: 2014 + input: + # Public Law 114-27, section 807, applies to taxable years beginning + # after December 31, 2014. + ctc_refundable_maximum: 2_000 + ctc: 2_000 + ctc_phase_out: 0 + ctc_phase_in: 1_500 + ctc_limiting_tax_liability: 0 + foreign_earned_income_exclusion: 30_000 + output: + refundable_ctc: 1_500 + +- name: Section 911 exclusion denies the refundable credit from 2015 + period: 2015 + input: + ctc_refundable_maximum: 2_000 + ctc: 2_000 + ctc_phase_out: 0 + ctc_phase_in: 1_500 + ctc_limiting_tax_liability: 0 + foreign_earned_income_exclusion: 30_000 + output: + refundable_ctc: 0 + +- name: In 2021, the fully refundable credit ignores the section 911 exclusion + period: 2021 + input: + # Section 24(i)(1)(A): subsection (d), including the section 911 bar, + # does not apply to a filer whose principal place of abode was in the + # United States for more than half of 2021 (Schedule 8812 Part I-B, + # which has no Form 2555 condition). The model assumes that test is met. + ctc_refundable_maximum: 6_000 + ctc: 6_000 + ctc_phase_out: 0 + ctc_phase_in: 0 + ctc_limiting_tax_liability: 0 + foreign_earned_income_exclusion: 30_000 + output: + refundable_ctc: 6_000 diff --git a/policyengine_us/tests/policy/reform/ctc_expansion.yaml b/policyengine_us/tests/policy/reform/ctc_expansion.yaml index d337d47b9fe..6ca07ff18a1 100644 --- a/policyengine_us/tests/policy/reform/ctc_expansion.yaml +++ b/policyengine_us/tests/policy/reform/ctc_expansion.yaml @@ -41,3 +41,20 @@ output: # 10,000 - 2,500 = 9,500 | 9,500 * 0.3 = 2,250 refundable_ctc: 2_250 + +- name: Section 911 exclusion still denies the refundable credit under the reform + period: 2024 + reforms: policyengine_us.reforms.congress.wyden_smith.ctc_expansion.ctc_expansion + input: + gov.contrib.congress.wyden_smith.per_child_actc_phase_in: True + gov.contrib.congress.wyden_smith.actc_lookback: True + tax_unit_earned_income: 8_000 + tax_unit_earned_income_last_year: 10_000 + ctc_qualifying_children: 2 + ctc: 4_000 + ctc_refundable_maximum: 3_200 # $1,600 * 2 + foreign_earned_income_exclusion: 20_000 + output: + # The bill amends section 24(h); section 24(d)(3) still applies. + # Without the exclusion this filer would get $2,250 (case above). + refundable_ctc: 0 diff --git a/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py b/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py new file mode 100644 index 00000000000..6fc99b91142 --- /dev/null +++ b/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py @@ -0,0 +1,182 @@ +"""Property tests for the section 911 bar on the refundable Child Tax Credit. + +26 U.S.C. 24(d)(3): "Paragraph (1) shall not apply to any taxpayer for any +taxable year if such taxpayer elects to exclude any amount from gross income +under section 911 for such taxable year." The 2025 Schedule 8812 +instructions (Part II-A) put it as "If you file Form 2555, you cannot claim +the additional child tax credit." + +Each batch of households is computed twice: under current law and under a +reform that switches the bar off, which reproduces the formula without it. +Households live in Texas and take the standard deduction, so the CTC's +tax-liability limit equals the tax actually owed. +""" + +from functools import cache + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st +from policyengine_core.reforms import Reform + +from policyengine_us import CountryTaxBenefitSystem, Simulation + +BAR = "gov.irs.credits.ctc.refundable.foreign_earned_income_exclusion_bar_applies" + + +@cache +def no_bar_system(): + # One reformed system serves every household batch. + reform = Reform.from_dict({BAR: {"2010-01-01.2100-12-31": False}}, country_id="us") + return CountryTaxBenefitSystem(reform=reform) + + +OUTPUTS = [ + "tax_unit_itemizes", + "foreign_earned_income_exclusion", + "income_tax_before_credits", + "ctc", + "ctc_refundable_maximum", + "refundable_ctc", + "non_refundable_ctc", + "income_tax", +] + + +def build_situation(households, year): + people, tax_units, marital_units = {}, {}, {} + groups = {"households": {}, "spm_units": {}, "families": {}} + for i, h in enumerate(households): + head = f"head_{i}" + people[head] = {"age": {year: 40}, "employment_income": {year: h["wages"]}} + members = [head] + marital_units[f"marital_unit_{i}"] = {"members": [head]} + if h["married"]: + spouse = f"spouse_{i}" + people[spouse] = {"age": {year: 40}} + members.append(spouse) + marital_units[f"marital_unit_{i}"]["members"].append(spouse) + for j, age in enumerate(h["dependent_ages"]): + dependent = f"dependent_{i}_{j}" + people[dependent] = {"age": {year: age}} + members.append(dependent) + marital_units[f"marital_unit_{i}_{j}"] = {"members": [dependent]} + tax_units[f"tax_unit_{i}"] = { + "members": members, + "foreign_earned_income_exclusion": {year: h["exclusion"]}, + } + 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} + return { + "people": people, + "tax_units": tax_units, + "marital_units": marital_units, + **groups, + } + + +def calculate(households, year, bar=True): + system = {} if bar else {"tax_benefit_system": no_bar_system()} + simulation = Simulation(situation=build_situation(households, year), **system) + return {v: np.asarray(simulation.calculate(v, year)) for v in OUTPUTS} + + +def assert_invariants(law, no_bar): + tol = 0.01 + assert not law["tax_unit_itemizes"].any() + excludes = law["foreign_earned_income_exclusion"] > 0 + # Section 24(d)(3): no refundable CTC for a section 911 filer. + assert (law["refundable_ctc"][excludes] == 0).all() + # Filers without the exclusion are untouched, bit for bit. + for v in ["refundable_ctc", "non_refundable_ctc", "income_tax"]: + assert np.array_equal(law[v][~excludes], no_bar[v][~excludes]), v + # The bar only ever removes refundable credit. + assert (law["refundable_ctc"] <= no_bar["refundable_ctc"]).all() + # Every dollar of CTC is either refundable or non-refundable. + for r in [law, no_bar]: + np.testing.assert_allclose( + r["refundable_ctc"] + r["non_refundable_ctc"], r["ctc"], atol=tol + ) + assert (r["refundable_ctc"] >= 0).all() + assert ( + r["refundable_ctc"] + <= np.minimum(r["ctc"], r["ctc_refundable_maximum"]) + tol + ).all() + # The CTC that offsets tax is the same either way (section 24(d)(1) + # refunds only what tax could not absorb), so the bar raises the tax by + # exactly the refund it denies. + np.testing.assert_allclose( + law["income_tax"] - no_bar["income_tax"], + np.where(excludes, no_bar["refundable_ctc"], 0), + atol=tol, + ) + + +def check(households, year=2025): + law = calculate(households, year) + no_bar = calculate(households, year, bar=False) + assert_invariants(law, no_bar) + return law, no_bar + + +GRID = [ + { + "married": married, + "dependent_ages": ages, + "wages": wages, + "exclusion": exclusion, + } + for married in (True, False) + for ages in ([], [5], [5, 8], [3, 6, 9], [17], [5, 17]) + for wages in (0, 8_000, 30_000, 60_000, 120_000, 420_000) + for exclusion in (0, 1, 50_000, 130_000) +] + + +def test_grid_invariants(): + law, no_bar = check(GRID) + excludes = law["foreign_earned_income_exclusion"] > 0 + # The grid reaches filers whose refund the bar removes. + assert (excludes & (no_bar["refundable_ctc"] > 0)).any() + + +def test_bar_applies_from_2015_but_not_in_2021(): + # In 2021, section 24(i)(1)(A) switched off all of subsection (d) for + # filers living in the United States, which the model assumes. + law = calculate(GRID, 2021) + no_bar = calculate(GRID, 2021, bar=False) + for v in ["refundable_ctc", "income_tax"]: + assert np.array_equal(law[v], no_bar[v]), v + # Public Law 114-27, section 807, applies to taxable years beginning + # after 2014 (refundable_ctc.yaml covers 2014: household simulations + # need parameters that start later). + law, no_bar = check(GRID, 2015) + excludes = law["foreign_earned_income_exclusion"] > 0 + assert (excludes & (no_bar["refundable_ctc"] > 0)).any() + + +household_strategy = st.fixed_dictionaries( + { + "married": st.booleans(), + "dependent_ages": st.lists(st.integers(0, 18), max_size=4), + "wages": st.integers(0, 500_000), + "exclusion": st.one_of(st.just(0), st.integers(1, 130_000)), + } +) + + +@settings( + max_examples=10, + deadline=None, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given( + st.lists(household_strategy, min_size=1, max_size=25), + st.sampled_from([2018, 2022, 2025, 2026]), +) +def test_random_households(households, year): + check(households, year) diff --git a/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc.py b/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc.py index 5b2f9bcaa95..1348aa6181a 100644 --- a/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc.py +++ b/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc.py @@ -8,7 +8,15 @@ class refundable_ctc(Variable): unit = USD documentation = "The portion of the Child Tax Credit that is refundable." definition_period = YEAR - reference = "https://www.law.cornell.edu/uscode/text/26/24#d" + reference = ( + "https://www.law.cornell.edu/uscode/text/26/24#d", + # No refundable CTC for filers electing a section 911 exclusion. + "https://www.law.cornell.edu/uscode/text/26/24#d_3", + # 2025 Instructions for Schedule 8812, Part II-A. + "https://www.irs.gov/pub/irs-pdf/i1040s8.pdf#page=3", + # 2021 Schedule 8812: Part I-B has no Form 2555 condition. + "https://www.irs.gov/pub/irs-prior/f1040s8--2021.pdf#page=1", + ) def formula(tax_unit, period, parameters): # This line corresponds to "the credit which would be allowed under this section [the CTC section]" @@ -24,6 +32,11 @@ def formula(tax_unit, period, parameters): total_ctc = tax_unit("ctc", period) if ctc.refundable.fully_refundable: + # Section 24(i)(1)(A) (2021) switched off all of subsection (d), + # including the section 911 exclusion bar, for filers whose + # principal place of abode was in the United States for more than + # half the year (Schedule 8812 Part I-B). The model assumes every + # filer meets that test. reduction = tax_unit("ctc_phase_out", period) reduced_max_amount = max_(0, maximum_amount - reduction) return min_(reduced_max_amount, total_ctc) @@ -35,4 +48,14 @@ def formula(tax_unit, period, parameters): ctc_capped_by_tax = min_(total_ctc, limiting_tax) ctc_capped_by_increased_tax = min_(total_ctc, limiting_tax + phase_in) amount_ctc_would_increase = ctc_capped_by_increased_tax - ctc_capped_by_tax - return min_(maximum_refundable_ctc, amount_ctc_would_increase) + refundable_amount = min_(maximum_refundable_ctc, amount_ctc_would_increase) + # Section 24(d)(3): the CTC is not refundable for a filer who elects to + # exclude any amount from gross income under section 911 (Form 2555). + elects_section_911_exclusion = ( + tax_unit("foreign_earned_income_exclusion", period) > 0 + ) + barred = ( + elects_section_911_exclusion + & ctc.refundable.foreign_earned_income_exclusion_bar_applies + ) + return where(barred, 0, refundable_amount) diff --git a/pyproject.toml b/pyproject.toml index ada6b6fa3ca..40ba2d3950f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ dev = [ "setuptools>=80.9.0", "build>=1.2.2.post1", "towncrier>=24.8.0", + "hypothesis>=6.100.0", ] [tool.hatch.build.targets.wheel] diff --git a/uv.lock b/uv.lock index b84171deda0..4f169a24605 100644 --- a/uv.lock +++ b/uv.lock @@ -766,6 +766,94 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/07/3d0c34c345043c6a398a5882e196b2220dc5861adfa18322448b90908f26/huggingface_hub-1.3.4-py3-none-any.whl", hash = "sha256:a0c526e76eb316e96a91e8a1a7a93cf66b0dd210be1a17bd5fc5ae53cba76bfd", size = 536611, upload-time = "2026-01-26T14:05:08.549Z" }, ] +[[package]] +name = "hypothesis" +version = "6.168.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/b7/13118bbc45d6d8b9d04e2de779e2a4ff23145ea39efa692b994fb874ca72/hypothesis-6.168.3.tar.gz", hash = "sha256:a43388f9067678fef6e13bdff325b6cfa6961a590498bb37f7ff31589c83bc75", size = 511022, upload-time = "2026-09-28T05:20:58.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/cd/746a2fc1e5e5ba34f07ea6ee1ad07525a3dde5ff4836db3a7980af49a891/hypothesis-6.168.3-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d20972ca134e652e9928ecd200966a8a2857adc2812c1d74b12a872e5cd503eb", size = 791536, upload-time = "2026-09-28T05:18:57.162Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0f/7a158e377b69556c8e12c25c8fd811d0103e54c24a12dab1f3b9819f202e/hypothesis-6.168.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:eafbec09d3e87d13d8242411d1f5868b5e879f1bd6d95e233e9ff80c527bea1c", size = 787313, upload-time = "2026-09-28T05:20:18.122Z" }, + { url = "https://files.pythonhosted.org/packages/45/c6/7df9104ee359e8fcd77791781c47e70794964ca69673665e0de2a6590f4c/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73c5627497968cc62d140e9fde1ea21e12cac7b64dc419fea8286cd34ff1ad4e", size = 1124036, upload-time = "2026-09-28T05:20:04.372Z" }, + { url = "https://files.pythonhosted.org/packages/92/0f/8a61715404a73b9a82cb1f976803428d603cdea976a9c30266c72ad6a7ce/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dc56e6dc6eeb0aeb08ac463f847279bde2336c4786faf7ee0af4b93cd031a7", size = 1147875, upload-time = "2026-09-28T05:19:15.495Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3f/2b16e95cc3b9a069afa23830a012aefe377ce4a457a227e5a5026eb827ff/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:753bb501f8560d2e321ed3b62596b4496c56f15668b0a0669231ccc3e6c4e80d", size = 1149489, upload-time = "2026-09-28T05:19:18.886Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e7/d7cf6dc068bb02732b2a6e38a35a0dcb7f2137608198fc79d740f69d197c/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71ab606c472449cb872ef2a7acaec679ee4f651b7f0a477a04ebc85d8933ef8c", size = 1191992, upload-time = "2026-09-28T05:19:21.83Z" }, + { url = "https://files.pythonhosted.org/packages/5f/26/f1e5b25dec15998e8375d722825ec3dae1075cbe7cb1f74221b2312a2e33/hypothesis-6.168.3-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26928956c54748e4dfa587333741ab750246123b93484955646ff316cca27eef", size = 1169911, upload-time = "2026-09-28T05:19:27.996Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/901234a49147e6bf8a5b9e54b775706a223c2b52befea1106ea2aaf15d48/hypothesis-6.168.3-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:0bdfc53c041b61c854fa3761735736b991bc2bddafee45a361cfe4d43027c1ef", size = 1129406, upload-time = "2026-09-28T05:18:42.632Z" }, + { url = "https://files.pythonhosted.org/packages/55/c1/5fc9ff91ec9fba6bb527833386c66812524b5cc75fe7d4a4f9122ee1c420/hypothesis-6.168.3-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:650528e2b1e2a45e95c2df624d4b9364b8ac5a027ba84e1eea2bc5398dd01dcd", size = 1160399, upload-time = "2026-09-28T05:20:23.911Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d2/49ad1ef5c55547c8abfd0fc571b3493806dda530fd5cb3d8263dbad86a72/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:209dc54cdb1b4d6d7020ad8d09e44c49756361b7183adacea4d6f5705085b595", size = 1299906, upload-time = "2026-09-28T05:18:58.417Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5f/f98a26094c4f462c4215807bed0f9a6b5508b27a3c329bcba1bc8adc7ac7/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:af8ca98cd11dc7f9427bb90483abcd9688d4df2e8e63893a30a2423b027ebb11", size = 1425538, upload-time = "2026-09-28T05:20:49.074Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b8/c5b4406e3a6591e51a42f85469351d1d824e0a54b84167aec9b2353759fe/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:f8122cfdd0bc0ba843063effa22ac310bdebdb3a1319bf1e63f14baa119c72ab", size = 1377084, upload-time = "2026-09-28T05:20:06.618Z" }, + { url = "https://files.pythonhosted.org/packages/e0/01/0d84a8ea469d024c15f602799c0b8410fbe03d99fdb0370449b33c3c916f/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4bedbb379eab34f792af7ee9a05aae04e9c08bbb52e0f90f5a2110d4fe4b2fbd", size = 1281162, upload-time = "2026-09-28T05:19:42.768Z" }, + { url = "https://files.pythonhosted.org/packages/22/b5/ea6435038de1a795f005cb603531e8ccc053febc50180f3ab55583c9c60e/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:5a953115b9f5c95133ab2d04efffeec96e5658c3207923dca7285f7db3e6bbef", size = 1300433, upload-time = "2026-09-28T05:18:55.634Z" }, + { url = "https://files.pythonhosted.org/packages/6c/78/fa6c77d9f64b6d592e69e582d83cbce7d7e56897faa4762885a2123b0166/hypothesis-6.168.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6173558e676ad25ed1e20507fa4024a0d816dd90f715f77006e5a28f19109026", size = 1336273, upload-time = "2026-09-28T05:19:08.273Z" }, + { url = "https://files.pythonhosted.org/packages/f1/56/4acd0d778bab818c9ea336fcba768bf43bc70db7f84cc373579298df4641/hypothesis-6.168.3-cp310-abi3-win32.whl", hash = "sha256:dc66390fb12d80585aa9222bf538ce8b7aa22cf5d118250647355a1c9e8f62f4", size = 678211, upload-time = "2026-09-28T05:19:23.355Z" }, + { url = "https://files.pythonhosted.org/packages/b0/db/0a02b146ad1c30f9716362f2e1a379dfd6b0f7be03ac1e22c9da24c9e2a9/hypothesis-6.168.3-cp310-abi3-win_amd64.whl", hash = "sha256:92325b276360fe86c5bf71a568c0d53a6d140b0de36dcf029f9164a17803bb24", size = 684906, upload-time = "2026-09-28T05:18:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c0/958deaf726848f96f52250740bf39f13f476b068e22f73e2b358eaa07532/hypothesis-6.168.3-cp310-abi3-win_arm64.whl", hash = "sha256:3cf6f1eeaf41cd8d60cf1f88fde905ca1dd77c906929a507d6ac7f66f2ccba2a", size = 683292, upload-time = "2026-09-28T05:19:57.404Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ab/443208c2988a24eaafbfbd6791b897ad9a8a1dd7f870d5b455d7d15a28ad/hypothesis-6.168.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9029775dc2e25e3e8e596c4306926f0079158353631c7c52c285bbc5c8073c2b", size = 792239, upload-time = "2026-09-28T05:19:59.144Z" }, + { url = "https://files.pythonhosted.org/packages/cf/7b/619c1a76c52de2d3cfd68a2f9876c09464d63097db5bf86a28b62dc37bb1/hypothesis-6.168.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0e1d91530cefdb9e0b6467c203eb509c61a11d70480a37db59cb53a2f9913102", size = 788113, upload-time = "2026-09-28T05:19:17.145Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dd/33841e8833105f850214a486d83740ba52041892ca94e687a05829c0ce83/hypothesis-6.168.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0df00cbe8133aa220308d11fa28e3add996cf5102a39b279c1d711015fd9114b", size = 1124241, upload-time = "2026-09-28T05:18:37.589Z" }, + { url = "https://files.pythonhosted.org/packages/4b/43/c84a7900feced6a420e824ab61d316e4f59a6887d991bc143ecc294a09ed/hypothesis-6.168.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd7f75a2e23288ee82ee965a952473d9c5447c2cbc1afc94be09d2402201774e", size = 1170406, upload-time = "2026-09-28T05:19:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/47/5d/6e2a32b9c3020af7a06b00fe446a2c5155ae76ac468c0b68d7188e524002/hypothesis-6.168.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc2b1c37631f2231dd0d077225aa5908fc2bd34a4c3faf475053b0bc2eb24c95", size = 1300339, upload-time = "2026-09-28T05:20:31.968Z" }, + { url = "https://files.pythonhosted.org/packages/f9/78/19f3c9eb9a5c215b9fbde57be18ef7c1c78afa509b460d4e8f86026d741a/hypothesis-6.168.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:73960a58f6efc8cbc57d8ad0647955f7c5b25f3d562e3eaa57afa1c69894f7aa", size = 1336484, upload-time = "2026-09-28T05:19:02.841Z" }, + { url = "https://files.pythonhosted.org/packages/88/bb/d46c4519e989e022667705d8a56e83a09ad4ea50eae07ca4bb0f4708dd23/hypothesis-6.168.3-cp311-cp311-win_amd64.whl", hash = "sha256:3f1122223759acc0fe5301c1ae505d762ee61af91db7b191a791ccdbcd965462", size = 684646, upload-time = "2026-09-28T05:20:00.842Z" }, + { url = "https://files.pythonhosted.org/packages/7a/56/44a17266bcf1add5fba7c3df5c3920396a5e924d917fb0534a6f9e2b9b9a/hypothesis-6.168.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bff12e036b67abc5ded682f724f4d7afaff5223190270a0763695f218c079068", size = 793313, upload-time = "2026-09-28T05:20:22.045Z" }, + { url = "https://files.pythonhosted.org/packages/d8/18/1a25b11e9133cb54c02ee6cf99a61296ad461aabc7aae3399e1d0ff1f095/hypothesis-6.168.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c73c6188056e6dc110260e39d141c69c3d02f23e3ba1d8610c7a8b6f2510ec96", size = 784855, upload-time = "2026-09-28T05:19:34.496Z" }, + { url = "https://files.pythonhosted.org/packages/8b/da/728d3210f0b0089337f37206a100d31ef546d1f99839e709f0a9ca443f8e/hypothesis-6.168.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb1063cb794765097b8c0add598bd34a904f44dc15db81275dd1f0ed1ac567ed", size = 1123043, upload-time = "2026-09-28T05:19:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/53/86/bfb9ec344a8b3ef2fa6f59bb3d30c5b3a7d86d594107670539c34ee53d1b/hypothesis-6.168.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b2d47f5d9060b049039bef0b267e88480f6468f95ee394e514e84a81ccdc5f5", size = 1169121, upload-time = "2026-09-28T05:20:40.512Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e0/5a76dbafbcaf8fff075f09377cddefa17289ef05dbf51878dde88385b966/hypothesis-6.168.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8367f623a98cbf90f33fd2a43b100671b152b5975bc343a578290945f52f7018", size = 1298749, upload-time = "2026-09-28T05:19:32.883Z" }, + { url = "https://files.pythonhosted.org/packages/28/15/0e65af96b353f2c4bdc283c53ed0980e5ac5009afe97ef3b23c77b364664/hypothesis-6.168.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd81241f4cd76fb18d481e87b9688b30a0cb5c30841730513323ba1c7aba1837", size = 1335271, upload-time = "2026-09-28T05:20:54.005Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c5/37eab2155664aa544df321f6d3940b89c6702d79a72fdc98d7b5a2286f8c/hypothesis-6.168.3-cp312-cp312-win_amd64.whl", hash = "sha256:377438de53afb94347d9845b7d90db5905c6d2a8d612108e9938e0f80503bb90", size = 682211, upload-time = "2026-09-28T05:18:51.206Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a2/6787da846d929e52fc3344d803299c45782dbeac287528af08380e984bf8/hypothesis-6.168.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1b230a850de63334c16654a34a2d547e0179d36b9071d4439b3e7237f6d077e7", size = 793254, upload-time = "2026-09-28T05:19:36.294Z" }, + { url = "https://files.pythonhosted.org/packages/89/ba/2893f5ca42501f4d562cba3229c8994c3a3e5fac66ef60c1e0e0b5220a5d/hypothesis-6.168.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d63b0226cd3e0d8bdd97c3384b22a21934ed4d53246c1c93575dada616672499", size = 784699, upload-time = "2026-09-28T05:18:54.194Z" }, + { url = "https://files.pythonhosted.org/packages/b7/aa/7d7349daf75b71f6f35876f8de115e974c5c04d600e86df1b2779b0883ab/hypothesis-6.168.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0369f5df055f96e117ab12e5f249668ff144731ab5280bc7a205fdf81f990b89", size = 1123016, upload-time = "2026-09-28T05:20:36.372Z" }, + { url = "https://files.pythonhosted.org/packages/00/f0/7774e1ea072708ea46cb25c4aeee5f9978b6c271764809069e8a6789858e/hypothesis-6.168.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f076bcd0f77fdcdb7797826c879099573d03a02e228ebe649ea917081b962ac0", size = 1168989, upload-time = "2026-09-28T05:20:08.44Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7d/113992abed9efbd7944e3a496a382da58152dce126fddf6419ff31a0a57d/hypothesis-6.168.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b823ba1fcec8da730f29316d010b06d3f7e0c3828dcf91020e24c55e7d24652a", size = 1298626, upload-time = "2026-09-28T05:19:40.986Z" }, + { url = "https://files.pythonhosted.org/packages/d5/65/3659fa5e733027e5b37a27486e57f4053535e40853d23501c422bb275043/hypothesis-6.168.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dd2849c269d674e4618590f3b48d443bd4c06b5aef3d8d869086c2e6d213d248", size = 1335184, upload-time = "2026-09-28T05:20:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/b4/6c/35aab2221b5ea65125340f236e1a765a9e9f3b28d89a389ea0033fed29f7/hypothesis-6.168.3-cp313-cp313-win_amd64.whl", hash = "sha256:3ef7d26f5789e691401d5f87eafed9bd2763f0dbe47af6d6b66012a509404766", size = 682173, upload-time = "2026-09-28T05:19:12.762Z" }, + { url = "https://files.pythonhosted.org/packages/6e/79/27b0cb56ff5d2bc92458bf6fcebdb1f0dc01f57f043a178e9f9d74498169/hypothesis-6.168.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e2d4c68729a13df9af4998d2652cfb5d541c5609b88c306880a5dfb284938ec2", size = 793287, upload-time = "2026-09-28T05:20:38.522Z" }, + { url = "https://files.pythonhosted.org/packages/e8/bd/5342f95c3bc36586777ef8cb8eba87ac7acf0184210327fb0e6d8654e295/hypothesis-6.168.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b345f818083ec99966a43ca4f7b38feb62c6920ce28572bc2bde4948a8b7eaba", size = 784857, upload-time = "2026-09-28T05:20:42.609Z" }, + { url = "https://files.pythonhosted.org/packages/4a/5f/fc774241518a0588d362680a3084275058aab86d6bc02a2c61e1073142d0/hypothesis-6.168.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0608c610fc002978fc5de0f471770d8817e9455cb8649a47983c9f8bccfc1897", size = 1123330, upload-time = "2026-09-28T05:19:29.735Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/131f16775a3dca5f4fe27f0d6ad9a9851600098a54a872d163f6ebf0b664/hypothesis-6.168.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dcf6448b1ecc37f2b23f2d1b3ddfc9ff6b6910614c15a642dc82f419b96037b", size = 1169178, upload-time = "2026-09-28T05:19:53.436Z" }, + { url = "https://files.pythonhosted.org/packages/06/61/c9f5bff8b73321c9fd12f2d69666c6e4861b97b60f5f24fdd6ac293007fd/hypothesis-6.168.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ae4f9f094041dcce5119ebd7bab71062743ab02b6e654d056b370beda78c19e2", size = 1299127, upload-time = "2026-09-28T05:20:14.486Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6d/d4618f8ab12dd76c4d58ea052252759aa2bf0c4e71f4b502ba5d577e0b9f/hypothesis-6.168.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d479985fe73af97badfb72dc6d20c6a353e486a36f6d065f600026e0cf954a86", size = 1335390, upload-time = "2026-09-28T05:20:10.326Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/727161e17cc297df1aeea945de05c532033b66751625bec192bc5b5ff707/hypothesis-6.168.3-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:785e2c45f8c08e274b4bf1ccae97f1a4e09407e9a68e80790cfab1d17a4a45fa", size = 624291, upload-time = "2026-09-28T05:18:36.376Z" }, + { url = "https://files.pythonhosted.org/packages/77/23/2f9b506392a0b8712440bcb781abc5713be9ffac1303b6d10f3669050882/hypothesis-6.168.3-cp314-cp314-win_amd64.whl", hash = "sha256:320920b1e3dae8611eee8a03d063cf2187446f2a17c38cfb8a7fc1466f71eee2", size = 682064, upload-time = "2026-09-28T05:20:16.336Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/efabfd95eb2b69c0c9fa1e1c83c8290aa2715d46baeb5a7764faf09c133a/hypothesis-6.168.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:35380baa981108a7f60c4eab71e46acd8d8f58440520346a1a6aba06dca7e074", size = 791875, upload-time = "2026-09-28T05:19:37.789Z" }, + { url = "https://files.pythonhosted.org/packages/1b/fc/2a0ada1623a9a33048bbf9f00a7c92513398c6b8865cba5efb854262ce30/hypothesis-6.168.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f0aaaed00438fa6673d856aaee12a4a8afbde82a9f3c5ff487cacfb064239afe", size = 783412, upload-time = "2026-09-28T05:18:48.266Z" }, + { url = "https://files.pythonhosted.org/packages/41/31/73c615d37eeb12da0209555612c32ced628d860267c23cfd2691c6207aea/hypothesis-6.168.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27e6df1576bf838e7f484d4ae5cba92114497ca30afb917056bf1ea33e95675", size = 1121662, upload-time = "2026-09-28T05:19:26.225Z" }, + { url = "https://files.pythonhosted.org/packages/92/54/1893344a3b8bbf83fa7c9bb7e96f6836580213854f46671cb749f7b4706c/hypothesis-6.168.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bc85014577982ec6d2e266edc5cd6e7a7d3674c648791ca983c67a52f89a0c4", size = 1167761, upload-time = "2026-09-28T05:18:49.85Z" }, + { url = "https://files.pythonhosted.org/packages/26/97/a61d82968febd25a0f08ffbbfabfaeead66f68fc5fa73f3c9ece1c36fc8f/hypothesis-6.168.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ccfc29505aa1cdcc254cb9cd701fd0811fef5480addd97d4127a00df023410f7", size = 1297309, upload-time = "2026-09-28T05:19:51.711Z" }, + { url = "https://files.pythonhosted.org/packages/bc/01/fe8e4cf6d39d02f4efadaa351427fa13722086cfc54a2da3579741c5177a/hypothesis-6.168.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32d0699566aaa93f9e97a44705d7164386f1e91de78d277f53bada35e78bcc96", size = 1334260, upload-time = "2026-09-28T05:19:00.121Z" }, + { url = "https://files.pythonhosted.org/packages/39/6a/09177ebc62f4778f94379ecade6a51299d3d8c4f4b75ba636bf3a0202364/hypothesis-6.168.3-cp314-cp314t-win_amd64.whl", hash = "sha256:28d88fa174ecbd4ecbd7bb290f06d0db084a3971c3f511ae2830b65b5e25500f", size = 681992, upload-time = "2026-09-28T05:19:48.043Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f6/890bf33d608cd63348d3146a3ca83362fbf09e589c5e5230a00c403070f4/hypothesis-6.168.3-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:61f5782d807b1e6aa5c9beef1054cb2037e7d1add48a64972cd6ee447778781f", size = 791231, upload-time = "2026-09-28T05:18:43.998Z" }, + { url = "https://files.pythonhosted.org/packages/03/dd/fc36b204f8aa7437c676576d9437f0422de46aa8a729e6cd5bbb0224e759/hypothesis-6.168.3-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:7aacf3cf40c7ce8f9e4924d5b57bc0b068beafdfed2347bcf160a28b4cfbba7b", size = 783171, upload-time = "2026-09-28T05:19:20.353Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d2/3cd087d577db67c404991d7723e64d7cd75570cdc82a0f7329aed6f2086c/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01768a03a30dc54df7fe457c0b34016c84d598fa00d5965ededab93ba4eb3408", size = 1121152, upload-time = "2026-09-28T05:18:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/42/6f/a05a68cc66e7a22701c50bf94fb9ddeb36f4f5d0de3cc130066709eda621/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1a8a4ffc6c6e6e577f2bfbcfebf7cffbb310283ba2532c729570a0a752cebaaa", size = 1144069, upload-time = "2026-09-28T05:20:44.705Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f4/fdd7a093fb2fb3a0ce24256c92ad5bf67936f4669bd06d5ceae4298150ec/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b987d73eba95183a7e59cca6d1925c588aa4307922d9852cc2b4d282a2ae4128", size = 1146692, upload-time = "2026-09-28T05:20:25.83Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/6dbd4377e935505ae4fc8ee4c7b18c69994c4bbaca015447c470218bdbee/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4569c39bd97d9573e946429ed676f3b55a7c8ed80920addd67d384a47d59b38", size = 1189480, upload-time = "2026-09-28T05:20:46.734Z" }, + { url = "https://files.pythonhosted.org/packages/ba/bf/ff288b496b690000d2686dcfa7f67855e4c5a6dddb464ed8e4880be83bb2/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6042b8707a4b25bbbfe20b110258fb7549a68951e5525608a8ce06091039e9fc", size = 1167109, upload-time = "2026-09-28T05:19:24.758Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d1/3fee2bc29fc747fabbb27e174515592e390809ca465d3bbbabbcfa3235a8/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:f413629de94d38a7a2ad259697a6752526143cb49c2e2d7bdba19381c80693f6", size = 1126823, upload-time = "2026-09-28T05:18:52.636Z" }, + { url = "https://files.pythonhosted.org/packages/58/6a/585294392fa6a6d9719446a042a777ba98ecca265d9e48cbedc10c88df0f/hypothesis-6.168.3-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f071737e4e775bebba07e319eb645a880d1e0186b4d24bad23255e849d86d483", size = 1155773, upload-time = "2026-09-28T05:20:51.268Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4d/fa283ff79996debf1ff08593e01f8b773eb8211b19b3d6d5d491317a65bf/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:f59a3912858d0609c26054aa1c474847c797937f5e0e1c77f0d3f08032e50f09", size = 1296671, upload-time = "2026-09-28T05:18:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/42/05/98f9c2f628da5afabb6c3e8df5d2d299d2d50bf306b14a6e26d3954c4951/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:f09c05a23a8025dd5cad07c2ed299a46778e72d49ff0dd5bf353bcc0f7dc1580", size = 1422001, upload-time = "2026-09-28T05:19:04.188Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c9/f2109ade29a7ec284d55bca328d784743e8b13e321576fd34ee7e65f99af/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:44ace770bda3a0301739fc5a413c764de790c739df1f1a7218dd049f8594d9f5", size = 1374182, upload-time = "2026-09-28T05:20:27.764Z" }, + { url = "https://files.pythonhosted.org/packages/ce/6d/e05d5f72441564a3bebc71fa155deafd0fd3b015d6014ec8a00edf6e42bb/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:03b131043608f94a2578a2a896a1a72092acb7079815eef5b8513b08447e0e62", size = 1278402, upload-time = "2026-09-28T05:20:56.332Z" }, + { url = "https://files.pythonhosted.org/packages/96/d7/6988a7f1f69c5c530687dce03a32c157e2d878e3a2e32af9269ce016b78a/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:e9784aca26eddfe99b03a0292320db742cc8a74200ef864fdce949f526f973cd", size = 1297770, upload-time = "2026-09-28T05:19:11.387Z" }, + { url = "https://files.pythonhosted.org/packages/a2/28/5bb82b60b836bd2329e1fe01ad94efc2b14dfa806f8e50ed14b795d78770/hypothesis-6.168.3-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:2b52ac363096232bebc2add117e9178d91f4248f4cdb919fd1026b5f86a4bb16", size = 1333977, upload-time = "2026-09-28T05:20:02.483Z" }, + { url = "https://files.pythonhosted.org/packages/8e/7d/d419841b8f65481ea1a50c4ba36f2670d48b8c7a51e4e8586089564cf7f6/hypothesis-6.168.3-cp315-abi3.abi3t-win32.whl", hash = "sha256:d28e3a6b511a74ce37df5274b51f36c2b274fea7365e00b16f0c81e22acd5957", size = 675394, upload-time = "2026-09-28T05:19:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/1de6a2ad100e44817f2e9a8e8ba3eeaf4769621f4f87f2d4966c4971fcc4/hypothesis-6.168.3-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:7b9638789548361a57d984f56619ac694a914c328181d911271618409261ff4a", size = 681699, upload-time = "2026-09-28T05:19:09.957Z" }, + { url = "https://files.pythonhosted.org/packages/c0/79/be3370fd02734d6b1d950183ae9224580d19fa8d356b95348d60f1e3eeb7/hypothesis-6.168.3-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:65d78e4357ec48ed2c67825f06740ee3599be4cfe770a6092bed07108d679ac5", size = 679849, upload-time = "2026-09-28T05:19:06.884Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0e/27960db1d45a94497e1681fb9d7b9140b44da4e655ba20f0d82f95d10230/hypothesis-6.168.3-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:101531b4ccf7fa12965a6b295d10b64c2f8a8fab61e1f9b18c55335d8fb02575", size = 793135, upload-time = "2026-09-28T05:19:14.056Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cb/be08379286c1925fcd651b92ed835f470a83b859b5c484b3431da9d851f5/hypothesis-6.168.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:41e0120814de5c3a6b58d8cb27cb11b8873ab33fba130cd09855a27f5a12a1ca", size = 788995, upload-time = "2026-09-28T05:20:34.164Z" }, + { url = "https://files.pythonhosted.org/packages/38/7b/6d885d139b2ed76e2f4908cf10eae56acc73bf05cbef286357c4559c9a04/hypothesis-6.168.3-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d120009d145697909f2f0c532f2d568558ddd6990707f8cba6576bfb9770696", size = 1124974, upload-time = "2026-09-28T05:19:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/05/cc/b302ae809b48254830cae57801d0af0b66783df515b5dd56b9f21c8dfa86/hypothesis-6.168.3-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:818b3d09bba60ef90463e47a4bc87275dba1e8aeb184d3102888440ca7c7837d", size = 1171967, upload-time = "2026-09-28T05:20:12.295Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ab/51031080a6600f5b07adf5f9dfa117345c2adda07a5c24bda523e70bd1e9/hypothesis-6.168.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:9e3c36eba80e089ecfa28ec08049723223cb41d3cfe4d9988169cf98440e402b", size = 685641, upload-time = "2026-09-28T05:19:44.609Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -1746,6 +1834,7 @@ dev = [ { name = "build" }, { name = "coverage" }, { name = "furo" }, + { name = "hypothesis" }, { name = "jupyter-book" }, { name = "ruff" }, { name = "setuptools" }, @@ -1757,6 +1846,7 @@ requires-dist = [ { name = "build", marker = "extra == 'dev'", specifier = ">=1.2.2.post1" }, { name = "coverage", marker = "extra == 'dev'", specifier = ">=7.9.2" }, { name = "furo", marker = "extra == 'dev'", specifier = ">=2024.8.6" }, + { name = "hypothesis", marker = "extra == 'dev'", specifier = ">=6.100.0" }, { name = "jupyter-book", marker = "extra == 'dev'", specifier = ">=1.0.4.post1" }, { name = "microdf-python", specifier = ">=1.0.0" }, { name = "pandas", specifier = ">=3.0" }, From 275bb1268473cd7bd08269d4a7d6e27b152c6d88 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 09:41:14 -0400 Subject: [PATCH 02/12] Skip the section 911 property tests when hypothesis is not installed Hypothesis is a dev extra. Importing it at module level would fail test collection in any environment installed without dev extras. Co-Authored-By: Claude Opus 5.5 --- .../test_ctc_foreign_earned_income_exclusion.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py b/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py index 6fc99b91142..d20fab85dff 100644 --- a/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py +++ b/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py @@ -15,12 +15,15 @@ from functools import cache import numpy as np -from hypothesis import HealthCheck, given, settings -from hypothesis import strategies as st +import pytest from policyengine_core.reforms import Reform from policyengine_us import CountryTaxBenefitSystem, Simulation +# Hypothesis is a dev extra; skip rather than fail collection without it. +hypothesis = pytest.importorskip("hypothesis") +st = pytest.importorskip("hypothesis.strategies") + BAR = "gov.irs.credits.ctc.refundable.foreign_earned_income_exclusion_bar_applies" @@ -169,12 +172,15 @@ def test_bar_applies_from_2015_but_not_in_2021(): ) -@settings( +@hypothesis.settings( max_examples=10, deadline=None, - suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], + suppress_health_check=[ + hypothesis.HealthCheck.too_slow, + hypothesis.HealthCheck.data_too_large, + ], ) -@given( +@hypothesis.given( st.lists(household_strategy, min_size=1, max_size=25), st.sampled_from([2018, 2022, 2025, 2026]), ) From e1966e26f3808c3641ac36d1f287832a91c976a3 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 16:34:10 -0400 Subject: [PATCH 03/12] Apply the section 911 bar to the Puerto Rico refundable CTC Review of the first version found two problems. pr_refundable_ctc takes the larger of refundable_ctc and social security taxes less the EITC, so the social security route handed the refund back to a filer the bar had just denied. Section 24(k)(2)(B) drops only the three-children condition for residents of Puerto Rico; it does not waive section 24(d)(3). The bar now lives in one variable, refundable_ctc_barred_by_section_911_exclusion, read by refundable_ctc, the Wyden-Smith reform and pr_refundable_ctc. The household test gave a tax figure the model computes without section 911(f), which taxes included income at the rates it would face with the excluded amount added back. The test now uses zero taxable income, where the Foreign Earned Income Tax Worksheet is skipped, so its figures match the form. Also fix two reference URLs fused by a missing comma in pr_refundable_ctc. Co-Authored-By: Claude Opus 5.5 --- ...e-foreign-earned-income-exclusion.fixed.md | 2 +- .../congress/wyden_smith/ctc_expansion.py | 8 +-- .../gov/irs/credits/ctc/integration.yaml | 52 +++++++++++-------- ...e_ctc_barred_by_section_911_exclusion.yaml | 49 +++++++++++++++++ .../refundable_ctc/pr_refundable_ctc.yaml | 41 +++++++++++++++ ...est_ctc_foreign_earned_income_exclusion.py | 24 +++++++-- .../credits/ctc/refundable/refundable_ctc.py | 17 +----- ...ble_ctc_barred_by_section_911_exclusion.py | 35 +++++++++++++ .../refundable_ctc/pr_refundable_ctc.py | 13 +++-- 9 files changed, 190 insertions(+), 51 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.yaml create mode 100644 policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.py diff --git a/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md b/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md index 7850caf502d..29ffa19bf63 100644 --- a/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md +++ b/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md @@ -1 +1 @@ -Deny the refundable Child Tax Credit to filers who elect to exclude foreign earned income or housing amounts under section 911 (26 U.S.C. 24(d)(3)), for tax years from 2015, including under the Wyden-Smith CTC expansion reform. +Deny the refundable Child Tax Credit to filers who elect to exclude foreign earned income or housing amounts under section 911 (26 U.S.C. 24(d)(3)), for tax years from 2015, including the Puerto Rico refundable Child Tax Credit and the Wyden-Smith CTC expansion reform. diff --git a/policyengine_us/reforms/congress/wyden_smith/ctc_expansion.py b/policyengine_us/reforms/congress/wyden_smith/ctc_expansion.py index a3d024cd7d5..c8aa5e56371 100644 --- a/policyengine_us/reforms/congress/wyden_smith/ctc_expansion.py +++ b/policyengine_us/reforms/congress/wyden_smith/ctc_expansion.py @@ -91,13 +91,7 @@ def formula(tax_unit, period, parameters): # The bill's CTC changes amend section 24(h) and leave section # 24(d)(3) in place: no refundable CTC for filers electing a # section 911 exclusion. - elects_section_911_exclusion = ( - tax_unit("foreign_earned_income_exclusion", period) > 0 - ) - barred = ( - elects_section_911_exclusion - & p_ctc.refundable.foreign_earned_income_exclusion_bar_applies - ) + barred = tax_unit("refundable_ctc_barred_by_section_911_exclusion", period) return where(barred, 0, refundable_amount) class reform(Reform): diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/integration.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/integration.yaml index a5047600abf..acc23623df3 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/integration.yaml @@ -487,16 +487,23 @@ ctc: 3_500 # 3000 for 10-year-old + 500 for 18-year-old (ODC) ctc_adult_individual_maximum: [0, 0, 500] # ODC for 18-year-old under ARPA +# The two cases below use a household with zero taxable income on purpose. +# The model does not apply the section 911(f) rule that taxes included income +# at the rates it would face with the excluded amount added back (Foreign +# Earned Income Tax Worksheet, Form 1040 line 16). That worksheet is skipped +# when taxable income is zero, so every figure here is also the form's. - name: Head of household with two children and a section 911 exclusion gets no additional child tax credit in 2025. period: 2025 - absolute_error_margin: 0 + absolute_error_margin: 0.01 input: people: parent: age: 40 # Wages included in gross income; the excluded foreign wages are # reported separately below. - employment_income: 60_000 + employment_income: 8_000 + # Over the $11,950 investment income limit, so no EITC either way. + taxable_interest_income: 12_500 child1: age: 5 child2: @@ -512,31 +519,32 @@ state_code: TX output: filing_status: HEAD_OF_HOUSEHOLD - adjusted_gross_income: 60_000 - # $60,000 less the $23,625 head of household standard deduction. - taxable_income: 36_375 - # 10% of $17,000 plus 12% of the $19,375 above it. - income_tax_before_credits: 4_025 + adjusted_gross_income: 20_500 + # $20,500 is below the $23,625 head of household standard deduction. + taxable_income: 0 + income_tax_before_credits: 0 + eitc: 0 # Schedule 8812 line 12: 2 x $2,200. ctc: 4_400 - # Line 13 (Credit Limit Worksheet A): $4,025 of tax, no other credits. - ctc_limiting_tax_liability: 4_025 + # Line 13 (Credit Limit Worksheet A): no tax to offset. + ctc_limiting_tax_liability: 0 + refundable_ctc_barred_by_section_911_exclusion: true # Part II-A: "If you file Form 2555, you cannot claim the additional - # child tax credit." Without Form 2555, line 27 would be $375. + # child tax credit." Without Form 2555, line 27 would be $825. refundable_ctc: 0 - # Line 14 allows $4,025 of the $4,400 against tax. non_refundable_ctc: 4_400 income_tax_before_refundable_credits: 0 income_tax: 0 -- name: The same household without a section 911 exclusion gets a $375 additional child tax credit in 2025. +- name: The same household without a section 911 exclusion gets an $825 additional child tax credit in 2025. period: 2025 - absolute_error_margin: 0 + absolute_error_margin: 0.01 input: people: parent: age: 40 - employment_income: 60_000 + employment_income: 8_000 + taxable_interest_income: 12_500 child1: age: 5 child2: @@ -550,12 +558,14 @@ members: [parent, child1, child2] state_code: TX output: - income_tax_before_credits: 4_025 + income_tax_before_credits: 0 + eitc: 0 ctc: 4_400 - # Part II-A: line 16a = $4,400 - $4,025 = $375; line 16b = 2 x $1,700 - # = $3,400; line 17 = $375; line 20 = 15% x ($60,000 - $2,500) = $8,625; - # line 27 = smaller of lines 17 and 20 = $375. - refundable_ctc: 375 - non_refundable_ctc: 4_025 + refundable_ctc_barred_by_section_911_exclusion: false + # Part II-A: line 16a = $4,400 - $0 = $4,400; line 16b = 2 x $1,700 + # = $3,400; line 17 = $3,400; line 20 = 15% x ($8,000 - $2,500) = $825; + # line 27 = smaller of lines 17 and 20 = $825. + refundable_ctc: 825 + non_refundable_ctc: 3_575 income_tax_before_refundable_credits: 0 - income_tax: -375 + income_tax: -825 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.yaml new file mode 100644 index 00000000000..3bb22883ff1 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.yaml @@ -0,0 +1,49 @@ +# Section 24(d)(3): "Paragraph (1) shall not apply to any taxpayer for any +# taxable year if such taxpayer elects to exclude any amount from gross +# income under section 911 for such taxable year." +- name: Any section 911 exclusion bars the refundable credit + period: 2025 + input: + foreign_earned_income_exclusion: 1 + output: + refundable_ctc_barred_by_section_911_exclusion: true + +- name: No section 911 exclusion, no bar + period: 2025 + input: + foreign_earned_income_exclusion: 0 + output: + refundable_ctc_barred_by_section_911_exclusion: false + +- name: The bar did not exist before 2015 + period: 2014 + input: + # Public Law 114-27, section 807, applies to taxable years beginning + # after December 31, 2014. + foreign_earned_income_exclusion: 30_000 + output: + refundable_ctc_barred_by_section_911_exclusion: false + +- name: The bar applies from 2015 + period: 2015 + input: + foreign_earned_income_exclusion: 30_000 + output: + refundable_ctc_barred_by_section_911_exclusion: true + +- name: No bar in the fully refundable year + period: 2021 + input: + # Section 24(i)(1)(A): subsection (d) does not apply to filers living in + # the United States or Puerto Rico, which the model assumes. + foreign_earned_income_exclusion: 30_000 + output: + refundable_ctc_barred_by_section_911_exclusion: false + +- name: The bar can be switched off + period: 2025 + input: + gov.irs.credits.ctc.refundable.foreign_earned_income_exclusion_bar_applies: false + foreign_earned_income_exclusion: 30_000 + output: + refundable_ctc_barred_by_section_911_exclusion: false diff --git a/policyengine_us/tests/policy/baseline/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc.yaml b/policyengine_us/tests/policy/baseline/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc.yaml index de0ada72255..4e36a06b1c9 100644 --- a/policyengine_us/tests/policy/baseline/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc.yaml @@ -19,3 +19,44 @@ ctc: 1_000 output: pr_refundable_ctc: 1_000 + +# Section 24(k)(2)(B) drops only the three-children condition for residents +# of Puerto Rico; section 24(d)(3) still denies the refundable credit to a +# filer electing a section 911 exclusion. +- name: Section 911 exclusion denies the credit despite social security taxes + period: 2024 + input: + eitc: 0 + uncapped_ssi: 0 + pr_refundable_ctc_social_security_tax: 1_530 + refundable_ctc: 0 + ctc: 2_000 + foreign_earned_income_exclusion: 1 + output: + pr_refundable_ctc: 0 + +- name: The same filer without a section 911 exclusion gets the social security tax amount + period: 2024 + input: + eitc: 0 + uncapped_ssi: 0 + pr_refundable_ctc_social_security_tax: 1_530 + refundable_ctc: 0 + ctc: 2_000 + foreign_earned_income_exclusion: 0 + output: + pr_refundable_ctc: 1_530 + +- name: In 2021, a section 911 exclusion does not deny the credit + period: 2021 + input: + # Section 24(i)(1) switched off subsection (d) for bona fide residents + # of Puerto Rico. + eitc: 0 + uncapped_ssi: 0 + pr_refundable_ctc_social_security_tax: 1_530 + refundable_ctc: 0 + ctc: 2_000 + foreign_earned_income_exclusion: 30_000 + output: + pr_refundable_ctc: 1_530 diff --git a/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py b/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py index d20fab85dff..18020ec466d 100644 --- a/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py +++ b/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py @@ -9,7 +9,12 @@ Each batch of households is computed twice: under current law and under a reform that switches the bar off, which reproduces the formula without it. Households live in Texas and take the standard deduction, so the CTC's -tax-liability limit equals the tax actually owed. +tax-liability limit equals the tax the model computes. + +The model does not apply the section 911(f) rule that taxes included income +at the rates it would face if the excluded amount were added back (the Foreign +Earned Income Tax Worksheet). The tax identity below is a property of the +model's liability, not a worksheet result. """ from functools import cache @@ -40,8 +45,10 @@ def no_bar_system(): "income_tax_before_credits", "ctc", "ctc_refundable_maximum", + "refundable_ctc_barred_by_section_911_exclusion", "refundable_ctc", "non_refundable_ctc", + "income_tax_capped_non_refundable_credits", "income_tax", ] @@ -92,18 +99,28 @@ def assert_invariants(law, no_bar): tol = 0.01 assert not law["tax_unit_itemizes"].any() excludes = law["foreign_earned_income_exclusion"] > 0 - # Section 24(d)(3): no refundable CTC for a section 911 filer. + barred = "refundable_ctc_barred_by_section_911_exclusion" + # Section 24(d)(3): the bar is exactly the filers with an exclusion, and + # none of them has a refundable CTC. + assert np.array_equal(law[barred], excludes) + assert not no_bar[barred].any() assert (law["refundable_ctc"][excludes] == 0).all() # Filers without the exclusion are untouched, bit for bit. for v in ["refundable_ctc", "non_refundable_ctc", "income_tax"]: assert np.array_equal(law[v][~excludes], no_bar[v][~excludes]), v # The bar only ever removes refundable credit. assert (law["refundable_ctc"] <= no_bar["refundable_ctc"]).all() - # Every dollar of CTC is either refundable or non-refundable. for r in [law, no_bar]: + # True by the definition of non_refundable_ctc; kept as a guard on it. np.testing.assert_allclose( r["refundable_ctc"] + r["non_refundable_ctc"], r["ctc"], atol=tol ) + # Section 26(a): the non-refundable credits allowed never exceed tax, + # even though a barred filer's non-refundable CTC is the whole credit. + assert ( + r["income_tax_capped_non_refundable_credits"] + <= np.maximum(r["income_tax_before_credits"], 0) + tol + ).all() assert (r["refundable_ctc"] >= 0).all() assert ( r["refundable_ctc"] @@ -154,6 +171,7 @@ def test_bar_applies_from_2015_but_not_in_2021(): no_bar = calculate(GRID, 2021, bar=False) for v in ["refundable_ctc", "income_tax"]: assert np.array_equal(law[v], no_bar[v]), v + assert not law["refundable_ctc_barred_by_section_911_exclusion"].any() # Public Law 114-27, section 807, applies to taxable years beginning # after 2014 (refundable_ctc.yaml covers 2014: household simulations # need parameters that start later). diff --git a/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc.py b/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc.py index 1348aa6181a..e2a65576a39 100644 --- a/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc.py +++ b/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc.py @@ -12,10 +12,6 @@ class refundable_ctc(Variable): "https://www.law.cornell.edu/uscode/text/26/24#d", # No refundable CTC for filers electing a section 911 exclusion. "https://www.law.cornell.edu/uscode/text/26/24#d_3", - # 2025 Instructions for Schedule 8812, Part II-A. - "https://www.irs.gov/pub/irs-pdf/i1040s8.pdf#page=3", - # 2021 Schedule 8812: Part I-B has no Form 2555 condition. - "https://www.irs.gov/pub/irs-prior/f1040s8--2021.pdf#page=1", ) def formula(tax_unit, period, parameters): @@ -32,11 +28,6 @@ def formula(tax_unit, period, parameters): total_ctc = tax_unit("ctc", period) if ctc.refundable.fully_refundable: - # Section 24(i)(1)(A) (2021) switched off all of subsection (d), - # including the section 911 exclusion bar, for filers whose - # principal place of abode was in the United States for more than - # half the year (Schedule 8812 Part I-B). The model assumes every - # filer meets that test. reduction = tax_unit("ctc_phase_out", period) reduced_max_amount = max_(0, maximum_amount - reduction) return min_(reduced_max_amount, total_ctc) @@ -51,11 +42,5 @@ def formula(tax_unit, period, parameters): refundable_amount = min_(maximum_refundable_ctc, amount_ctc_would_increase) # Section 24(d)(3): the CTC is not refundable for a filer who elects to # exclude any amount from gross income under section 911 (Form 2555). - elects_section_911_exclusion = ( - tax_unit("foreign_earned_income_exclusion", period) > 0 - ) - barred = ( - elects_section_911_exclusion - & ctc.refundable.foreign_earned_income_exclusion_bar_applies - ) + barred = tax_unit("refundable_ctc_barred_by_section_911_exclusion", period) return where(barred, 0, refundable_amount) diff --git a/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.py b/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.py new file mode 100644 index 00000000000..d0d73972e5c --- /dev/null +++ b/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.py @@ -0,0 +1,35 @@ +from policyengine_us.model_api import * + + +class refundable_ctc_barred_by_section_911_exclusion(Variable): + value_type = bool + entity = TaxUnit + label = "Refundable CTC barred by a section 911 exclusion" + documentation = ( + "Whether section 24(d)(3) denies the refundable Child Tax Credit " + "because the filer elects to exclude foreign earned income or housing " + "amounts from gross income under section 911 (Form 2555)." + ) + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/24#d_3", + # 2025 Instructions for Schedule 8812, Part II-A. + "https://www.irs.gov/pub/irs-pdf/i1040s8.pdf#page=3", + # 2021 Schedule 8812: Part I-B has no Form 2555 condition. + "https://www.irs.gov/pub/irs-prior/f1040s8--2021.pdf#page=1", + ) + + def formula(tax_unit, period, parameters): + p = parameters(period).gov.irs.credits.ctc.refundable + elects_section_911_exclusion = ( + tax_unit("foreign_earned_income_exclusion", period) > 0 + ) + # Section 24(i)(1)(A) (2021) switched off all of subsection (d), + # including this bar, for filers whose principal place of abode was in + # the United States for more than half the year and for bona fide + # residents of Puerto Rico. The model assumes every filer meets that + # test in a fully refundable year. + bar_in_force = ( + p.foreign_earned_income_exclusion_bar_applies and not p.fully_refundable + ) + return elects_section_911_exclusion & bar_in_force diff --git a/policyengine_us/variables/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc.py b/policyengine_us/variables/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc.py index 4b21fc7c2d7..d10336b6e05 100644 --- a/policyengine_us/variables/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc.py +++ b/policyengine_us/variables/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc.py @@ -8,8 +8,10 @@ class pr_refundable_ctc(Variable): 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.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#k_2_B", + "https://www.law.cornell.edu/uscode/text/26/24#d_3", ) # This provision is part of the federal CTC legal code @@ -30,4 +32,9 @@ def formula(tax_unit, period, parameters): base_credit = max_(tax_unit("refundable_ctc", period), reduced_ss_tax) # line 27 - return min_(base_credit, tax_unit("ctc", period)) + credit = min_(base_credit, tax_unit("ctc", period)) + # Section 24(k)(2)(B) drops only the three-children condition for + # residents of Puerto Rico. Section 24(d)(3) still denies the + # refundable credit to a filer electing a section 911 exclusion. + barred = tax_unit("refundable_ctc_barred_by_section_911_exclusion", period) + return where(barred, 0, credit) From 1c5d82f5034966b8653ffcecf608493aa5e411f0 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 23:16:39 -0400 Subject: [PATCH 04/12] Count the section 911 exclusion in the filing requirement; review fixes Second review of this PR (APPROVE WITH CHANGES) found: - With the bar on, a Form 2555 filer whose only reason to file was the refundable credit could become a model non-filer, which zeroes the premium tax credit and switches on Medicaid non-filer rules. The root cause predates this PR: 26 U.S.C. 6012(c) computes gross income for the filing requirement "without regard to the exclusion provided for in section 911", and tax_unit_is_required_to_file did not. It now adds the exclusion to gross income (as earned income, not unearned). - The household test's counterfactual and "every figure is the form's" wording overclaimed; it now asserts the section 26(a)-limited credits. - The social security case did not route through ctc_phase_in; it now computes the phase-in from social security taxes. - The 2021 Puerto Rico case used an input combination 2021 law rules out. - The property test replaces a 26(a) check that could not fail with the equality of the limited credits under the bar and without it, covers pr_refundable_ctc, and caps dependants at 17. Also: changelog and parameter text name the 2021 exception, cite 24(k)(2)(B)(ii) and the prior-year Schedule 8812 instructions URL, fix two fused reference URLs, and ignore .hypothesis/. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 1 + ...e-foreign-earned-income-exclusion.fixed.md | 2 +- ...n_earned_income_exclusion_bar_applies.yaml | 4 +- .../gov/irs/credits/ctc/integration.yaml | 12 ++++-- .../ctc/refundable/refundable_ctc.yaml | 20 ++++++--- .../gov/irs/tax_unit/tax_unit_is_filer.yaml | 42 +++++++++++++++++++ .../refundable_ctc/pr_refundable_ctc.yaml | 6 +-- ...est_ctc_foreign_earned_income_exclusion.py | 33 ++++++++++----- ...ble_ctc_barred_by_section_911_exclusion.py | 2 +- .../gov/irs/tax_unit_is_required_to_file.py | 20 +++++++-- .../refundable_ctc/pr_refundable_ctc.py | 4 +- .../pr_refundable_ctc_social_security_tax.py | 4 +- 12 files changed, 116 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index d0c9b065baf..364053d7b7e 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ venv/ # Python caches .pytest_cache +.hypothesis **/__pycache__ # Data diff --git a/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md b/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md index 29ffa19bf63..e378f992e19 100644 --- a/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md +++ b/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md @@ -1 +1 @@ -Deny the refundable Child Tax Credit to filers who elect to exclude foreign earned income or housing amounts under section 911 (26 U.S.C. 24(d)(3)), for tax years from 2015, including the Puerto Rico refundable Child Tax Credit and the Wyden-Smith CTC expansion reform. +Deny the refundable Child Tax Credit to filers who elect to exclude foreign earned income or housing amounts under section 911 (26 U.S.C. 24(d)(3)) for tax years from 2015 except fully refundable 2021, including the Puerto Rico refundable Child Tax Credit and the Wyden-Smith CTC expansion reform, and count the section 911 exclusion in gross income for the filing requirement (26 U.S.C. 6012(c)). diff --git a/policyengine_us/parameters/gov/irs/credits/ctc/refundable/foreign_earned_income_exclusion_bar_applies.yaml b/policyengine_us/parameters/gov/irs/credits/ctc/refundable/foreign_earned_income_exclusion_bar_applies.yaml index 15b1c7a0fb5..b538937049e 100644 --- a/policyengine_us/parameters/gov/irs/credits/ctc/refundable/foreign_earned_income_exclusion_bar_applies.yaml +++ b/policyengine_us/parameters/gov/irs/credits/ctc/refundable/foreign_earned_income_exclusion_bar_applies.yaml @@ -1,4 +1,4 @@ -description: The IRS denies the refundable Child Tax Credit to filers who elect to exclude any amount from gross income under section 911 (the foreign earned income and housing exclusions) if this is true. +description: The IRS denies the refundable Child Tax Credit to filers who elect to exclude any amount from gross income under section 911 (the foreign earned income and housing exclusions) if this is true. It has no effect when the credit is fully refundable (2021). values: 2013-01-01: false 2015-01-01: true @@ -15,4 +15,4 @@ metadata: - title: Public Law 114-27, § 807 href: https://www.govinfo.gov/content/pkg/PLAW-114publ27/pdf/PLAW-114publ27.pdf#page=58 - title: 2025 Instructions for Schedule 8812, Part II-A - href: https://www.irs.gov/pub/irs-pdf/i1040s8.pdf#page=3 + href: https://www.irs.gov/pub/irs-prior/i1040s8--2025.pdf#page=3 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/integration.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/integration.yaml index acc23623df3..b07c3de8274 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/integration.yaml @@ -491,7 +491,10 @@ # The model does not apply the section 911(f) rule that taxes included income # at the rates it would face with the excluded amount added back (Foreign # Earned Income Tax Worksheet, Form 1040 line 16). That worksheet is skipped -# when taxable income is zero, so every figure here is also the form's. +# when taxable income is zero, so the tax and Schedule 8812 line 12, 14 and 27 +# figures here are the form's. non_refundable_ctc is the credit before the +# section 26(a) limit; the limited amount is +# income_tax_capped_non_refundable_credits (line 14 here: $0). - name: Head of household with two children and a section 911 exclusion gets no additional child tax credit in 2025. period: 2025 absolute_error_margin: 0.01 @@ -530,13 +533,15 @@ ctc_limiting_tax_liability: 0 refundable_ctc_barred_by_section_911_exclusion: true # Part II-A: "If you file Form 2555, you cannot claim the additional - # child tax credit." Without Form 2555, line 27 would be $825. + # child tax credit." A household with the same included income and no + # foreign earnings gets $825 (next case). refundable_ctc: 0 non_refundable_ctc: 4_400 + income_tax_capped_non_refundable_credits: 0 income_tax_before_refundable_credits: 0 income_tax: 0 -- name: The same household without a section 911 exclusion gets an $825 additional child tax credit in 2025. +- name: A household with the same included income and no foreign earnings gets an $825 additional child tax credit in 2025. period: 2025 absolute_error_margin: 0.01 input: @@ -567,5 +572,6 @@ # line 27 = smaller of lines 17 and 20 = $825. refundable_ctc: 825 non_refundable_ctc: 3_575 + income_tax_capped_non_refundable_credits: 0 income_tax_before_refundable_credits: 0 income_tax: -825 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/refundable_ctc.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/refundable_ctc.yaml index 1c690eab327..efeec12296e 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/refundable_ctc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/refundable_ctc.yaml @@ -102,28 +102,36 @@ - name: Section 911 exclusion denies the refundable credit from social security taxes period: 2025 input: - # Three or more children: the phase-in amount reflects social security - # taxes less the EITC (section 24(d)(1)(B)(ii)), which section 24(d)(3) - # also switches off. + # Three children and no earnings over the threshold: the phase-in amount + # is social security taxes less the EITC (section 24(d)(1)(B)(ii)), which + # section 24(d)(3) also switches off. + ctc_qualifying_children: 3 + ctc_phase_in_relevant_earnings: 0 + ctc_social_security_tax: 4_000 + eitc: 0 ctc_refundable_maximum: 5_100 ctc: 6_600 ctc_phase_out: 0 - ctc_phase_in: 4_000 ctc_limiting_tax_liability: 1_000 foreign_earned_income_exclusion: 30_000 output: + ctc_phase_in: 4_000 refundable_ctc: 0 -- name: No section 911 exclusion keeps the refundable credit +- name: No section 911 exclusion keeps the refundable credit from social security taxes period: 2025 input: + ctc_qualifying_children: 3 + ctc_phase_in_relevant_earnings: 0 + ctc_social_security_tax: 4_000 + eitc: 0 ctc_refundable_maximum: 5_100 ctc: 6_600 ctc_phase_out: 0 - ctc_phase_in: 4_000 ctc_limiting_tax_liability: 1_000 foreign_earned_income_exclusion: 0 output: + ctc_phase_in: 4_000 # min(5,100, min(6,600, 1,000 + 4,000) - min(6,600, 1,000)) = 4,000. refundable_ctc: 4_000 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax_unit/tax_unit_is_filer.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax_unit/tax_unit_is_filer.yaml index 7eb87041afb..e56e7fe5165 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax_unit/tax_unit_is_filer.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax_unit/tax_unit_is_filer.yaml @@ -57,3 +57,45 @@ employment_income: 0 output: tax_unit_is_filer: false + +# 26 U.S.C. 6012(c): for the filing requirement, gross income is computed +# without regard to the section 911 exclusion. +- name: Excluded foreign earnings count toward the filing threshold + period: 2025 + input: + filing_status: SINGLE + age_head: 40 + employment_income: 5_000 + foreign_earned_income_exclusion: 30_000 + would_file_if_eligible_for_refundable_credit: false + would_file_taxes_voluntarily: false + output: + # $35,000 of gross income under section 6012(c), over the $15,750 + # single standard deduction. + tax_unit_is_required_to_file: true + tax_unit_is_filer: true + +- name: The same filer without excluded foreign earnings is under the threshold + period: 2025 + input: + filing_status: SINGLE + age_head: 40 + employment_income: 5_000 + foreign_earned_income_exclusion: 0 + would_file_if_eligible_for_refundable_credit: false + would_file_taxes_voluntarily: false + output: + tax_unit_is_required_to_file: false + tax_unit_is_filer: false + +- name: Excluded foreign earnings do not count as unearned income + period: 2025 + input: + filing_status: SINGLE + age_head: 40 + foreign_earned_income_exclusion: 10_000 + would_file_if_eligible_for_refundable_credit: false + would_file_taxes_voluntarily: false + output: + # $10,000 is under the $15,750 threshold, and none of it is unearned. + tax_unit_is_required_to_file: false diff --git a/policyengine_us/tests/policy/baseline/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc.yaml b/policyengine_us/tests/policy/baseline/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc.yaml index 4e36a06b1c9..8bb4d6fcf98 100644 --- a/policyengine_us/tests/policy/baseline/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc.yaml @@ -51,12 +51,12 @@ period: 2021 input: # Section 24(i)(1) switched off subsection (d) for bona fide residents - # of Puerto Rico. + # of Puerto Rico, so the whole credit was refundable. eitc: 0 uncapped_ssi: 0 pr_refundable_ctc_social_security_tax: 1_530 - refundable_ctc: 0 + refundable_ctc: 2_000 ctc: 2_000 foreign_earned_income_exclusion: 30_000 output: - pr_refundable_ctc: 1_530 + pr_refundable_ctc: 2_000 diff --git a/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py b/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py index 18020ec466d..b2231c2b144 100644 --- a/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py +++ b/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py @@ -49,6 +49,8 @@ def no_bar_system(): "refundable_ctc", "non_refundable_ctc", "income_tax_capped_non_refundable_credits", + # No residence test in the formula, so it is computed for every household. + "pr_refundable_ctc", "income_tax", ] @@ -105,8 +107,15 @@ def assert_invariants(law, no_bar): assert np.array_equal(law[barred], excludes) assert not no_bar[barred].any() assert (law["refundable_ctc"][excludes] == 0).all() + # The Puerto Rico credit's social security route is barred as well. + assert (law["pr_refundable_ctc"][excludes] == 0).all() # Filers without the exclusion are untouched, bit for bit. - for v in ["refundable_ctc", "non_refundable_ctc", "income_tax"]: + for v in [ + "refundable_ctc", + "non_refundable_ctc", + "pr_refundable_ctc", + "income_tax", + ]: assert np.array_equal(law[v][~excludes], no_bar[v][~excludes]), v # The bar only ever removes refundable credit. assert (law["refundable_ctc"] <= no_bar["refundable_ctc"]).all() @@ -115,20 +124,20 @@ def assert_invariants(law, no_bar): np.testing.assert_allclose( r["refundable_ctc"] + r["non_refundable_ctc"], r["ctc"], atol=tol ) - # Section 26(a): the non-refundable credits allowed never exceed tax, - # even though a barred filer's non-refundable CTC is the whole credit. - assert ( - r["income_tax_capped_non_refundable_credits"] - <= np.maximum(r["income_tax_before_credits"], 0) + tol - ).all() assert (r["refundable_ctc"] >= 0).all() assert ( r["refundable_ctc"] <= np.minimum(r["ctc"], r["ctc_refundable_maximum"]) + tol ).all() - # The CTC that offsets tax is the same either way (section 24(d)(1) - # refunds only what tax could not absorb), so the bar raises the tax by - # exactly the refund it denies. + # The CTC that offsets tax is the same either way: section 24(d)(1) refunds + # only what tax could not absorb, so the section 26(a)-limited credits are + # equal even though a barred filer's non-refundable CTC is the whole + # credit. The bar therefore raises the tax by exactly the refund it denies. + np.testing.assert_allclose( + law["income_tax_capped_non_refundable_credits"], + no_bar["income_tax_capped_non_refundable_credits"], + atol=tol, + ) np.testing.assert_allclose( law["income_tax"] - no_bar["income_tax"], np.where(excludes, no_bar["refundable_ctc"], 0), @@ -183,7 +192,9 @@ def test_bar_applies_from_2015_but_not_in_2021(): household_strategy = st.fixed_dictionaries( { "married": st.booleans(), - "dependent_ages": st.lists(st.integers(0, 18), max_size=4), + # Age 17 at most: the model treats an unmarried filer's 18-year-old + # as a spouse. + "dependent_ages": st.lists(st.integers(0, 17), max_size=4), "wages": st.integers(0, 500_000), "exclusion": st.one_of(st.just(0), st.integers(1, 130_000)), } diff --git a/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.py b/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.py index d0d73972e5c..1c48fa15c50 100644 --- a/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.py +++ b/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.py @@ -14,7 +14,7 @@ class refundable_ctc_barred_by_section_911_exclusion(Variable): reference = ( "https://www.law.cornell.edu/uscode/text/26/24#d_3", # 2025 Instructions for Schedule 8812, Part II-A. - "https://www.irs.gov/pub/irs-pdf/i1040s8.pdf#page=3", + "https://www.irs.gov/pub/irs-prior/i1040s8--2025.pdf#page=3", # 2021 Schedule 8812: Part I-B has no Form 2555 condition. "https://www.irs.gov/pub/irs-prior/f1040s8--2021.pdf#page=1", ) diff --git a/policyengine_us/variables/gov/irs/tax_unit_is_required_to_file.py b/policyengine_us/variables/gov/irs/tax_unit_is_required_to_file.py index cf1f3f204cb..5c38c142427 100644 --- a/policyengine_us/variables/gov/irs/tax_unit_is_required_to_file.py +++ b/policyengine_us/variables/gov/irs/tax_unit_is_required_to_file.py @@ -11,10 +11,19 @@ class tax_unit_is_required_to_file(Variable): status. """ definition_period = YEAR - reference = "https://www.law.cornell.edu/uscode/text/26/6012" + reference = ( + "https://www.law.cornell.edu/uscode/text/26/6012", + # Gross income counts amounts excluded under section 911. + "https://www.law.cornell.edu/uscode/text/26/6012#c", + ) def formula(tax_unit, period, parameters): - gross_income = add(tax_unit, period, ["irs_gross_income"]) + # Section 6012(c): for the filing requirement, gross income is + # computed without regard to the section 911 exclusion. + section_911_exclusion = tax_unit("foreign_earned_income_exclusion", period) + gross_income = add(tax_unit, period, ["irs_gross_income"]) + ( + section_911_exclusion + ) p = parameters(period).gov.irs.income.exemption exemption_amount = 0 if p.suspended else p.amount @@ -34,7 +43,12 @@ def formula(tax_unit, period, parameters): unearned_income_threshold = 500 + tax_unit( "additional_standard_deduction", period ) - unearned_income = gross_income - add(tax_unit, period, ["earned_income"]) + # The section 911 exclusion covers earned income only. + unearned_income = ( + gross_income + - add(tax_unit, period, ["earned_income"]) + - section_911_exclusion + ) unearned_income_over_threshold = unearned_income > unearned_income_threshold return income_over_exemption_amount | unearned_income_over_threshold diff --git a/policyengine_us/variables/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc.py b/policyengine_us/variables/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc.py index d10336b6e05..37afe4f2451 100644 --- a/policyengine_us/variables/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc.py +++ b/policyengine_us/variables/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc.py @@ -10,7 +10,7 @@ class pr_refundable_ctc(Variable): 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#k_2_B", + "https://www.law.cornell.edu/uscode/text/26/24#k_2_B_ii", "https://www.law.cornell.edu/uscode/text/26/24#d_3", ) @@ -33,7 +33,7 @@ def formula(tax_unit, period, parameters): # line 27 credit = min_(base_credit, tax_unit("ctc", period)) - # Section 24(k)(2)(B) drops only the three-children condition for + # Section 24(k)(2)(B)(ii) drops only the three-children condition for # residents of Puerto Rico. Section 24(d)(3) still denies the # refundable credit to a filer electing a section 911 exclusion. barred = tax_unit("refundable_ctc_barred_by_section_911_exclusion", period) 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 dec8a83bf8c..105f3bc114e 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 @@ -10,8 +10,8 @@ class pr_refundable_ctc_social_security_tax(Variable): 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.irs.gov/pub/irs-pdf/f1040s8.pdf", + "https://www.law.cornell.edu/uscode/text/26/24#h_4_A", ) # line 23 From 3bd638d04ea8b2b8453dc5b26879d791b74d80f6 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 07:42:30 -0400 Subject: [PATCH 05/12] Deny the EITC to section 911 claimants (26 U.S.C. 32(c)(1)(C)) Third review of this PR found that the section 6012(c) filing fix made a Form 2555 filer a required filer, which let the EITC through: the model's EITC lacked section 32(c)(1)(C), "The term 'eligible individual' does not include any individual who claims the benefits of section 911". In the new filing fixture (single, $5,000 included wages, $30,000 excluded) the EITC went from $0 to $382.50. eitc_eligible now excludes a tax unit with a positive foreign_earned_income_exclusion. Unlike the CTC bar, the rule has no 2015 start or 2021 exception, so it does not reuse the CTC bar variable. Also: the deterministic grid tests move out of the Hypothesis module so they run without the dev extra, the property tests assert a zero EITC for section 911 claimants, and a Puerto Rico test comment cites 24(k)(2)(B)(ii). Co-Authored-By: Claude Opus 5.5 --- ...e-foreign-earned-income-exclusion.fixed.md | 2 +- .../credits/earned_income/eitc_eligible.yaml | 37 +++++++++++++++ .../gov/irs/tax_unit/tax_unit_is_filer.yaml | 3 ++ .../refundable_ctc/pr_refundable_ctc.yaml | 2 +- ...est_ctc_foreign_earned_income_exclusion.py | 46 +++++-------------- ...oreign_earned_income_exclusion_property.py | 39 ++++++++++++++++ .../eligibility/eitc_eligible.py | 16 ++++++- 7 files changed, 107 insertions(+), 38 deletions(-) create mode 100644 policyengine_us/tests/test_ctc_foreign_earned_income_exclusion_property.py diff --git a/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md b/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md index e378f992e19..787e7932afb 100644 --- a/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md +++ b/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md @@ -1 +1 @@ -Deny the refundable Child Tax Credit to filers who elect to exclude foreign earned income or housing amounts under section 911 (26 U.S.C. 24(d)(3)) for tax years from 2015 except fully refundable 2021, including the Puerto Rico refundable Child Tax Credit and the Wyden-Smith CTC expansion reform, and count the section 911 exclusion in gross income for the filing requirement (26 U.S.C. 6012(c)). +Deny the refundable Child Tax Credit to filers who elect to exclude foreign earned income or housing amounts under section 911 (26 U.S.C. 24(d)(3)) for tax years from 2015 except fully refundable 2021, including the Puerto Rico refundable Child Tax Credit and the Wyden-Smith CTC expansion reform; deny the EITC to filers who claim the benefits of section 911 (26 U.S.C. 32(c)(1)(C)); and count the section 911 exclusion in gross income for the filing requirement (26 U.S.C. 6012(c)). diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_eligible.yaml index fc13da2b3ba..79a909e9c37 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/earned_income/eitc_eligible.yaml @@ -37,3 +37,40 @@ filing_status: JOINT output: eitc_eligible: true + +# 26 U.S.C. 32(c)(1)(C): "The term 'eligible individual' does not include any +# individual who claims the benefits of section 911 (relating to citizens or +# residents living abroad) for the taxable year." +- name: Section 911 claimant is not eligible. + period: 2025 + input: + eitc_investment_income_eligible: true + eitc_demographic_eligible: true + filer_meets_eitc_identification_requirements: true + filing_status: JOINT + foreign_earned_income_exclusion: 1 + output: + eitc_eligible: false + +- name: Without a section 911 exclusion the same filer is eligible. + period: 2025 + input: + eitc_investment_income_eligible: true + eitc_demographic_eligible: true + filer_meets_eitc_identification_requirements: true + filing_status: JOINT + foreign_earned_income_exclusion: 0 + output: + eitc_eligible: true + +- name: Section 911 claimant is not eligible in 2021 either. + period: 2021 + input: + # Unlike the CTC bar, section 32(c)(1)(C) has no 2021 exception. + eitc_investment_income_eligible: true + eitc_demographic_eligible: true + filer_meets_eitc_identification_requirements: true + filing_status: SINGLE + foreign_earned_income_exclusion: 30_000 + output: + eitc_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax_unit/tax_unit_is_filer.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax_unit/tax_unit_is_filer.yaml index e56e7fe5165..f55fec750d5 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax_unit/tax_unit_is_filer.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax_unit/tax_unit_is_filer.yaml @@ -74,6 +74,9 @@ # single standard deduction. tax_unit_is_required_to_file: true tax_unit_is_filer: true + # Filing does not make a section 911 claimant eligible for the EITC + # (section 32(c)(1)(C)). + eitc: 0 - name: The same filer without excluded foreign earnings is under the threshold period: 2025 diff --git a/policyengine_us/tests/policy/baseline/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc.yaml b/policyengine_us/tests/policy/baseline/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc.yaml index 8bb4d6fcf98..0f66bd6ef35 100644 --- a/policyengine_us/tests/policy/baseline/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc.yaml @@ -20,7 +20,7 @@ output: pr_refundable_ctc: 1_000 -# Section 24(k)(2)(B) drops only the three-children condition for residents +# Section 24(k)(2)(B)(ii) drops only the three-children condition for residents # of Puerto Rico; section 24(d)(3) still denies the refundable credit to a # filer electing a section 911 exclusion. - name: Section 911 exclusion denies the credit despite social security taxes diff --git a/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py b/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py index b2231c2b144..3b8b38a8feb 100644 --- a/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py +++ b/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py @@ -1,4 +1,4 @@ -"""Property tests for the section 911 bar on the refundable Child Tax Credit. +"""Invariants for the section 911 bar on the refundable Child Tax Credit. 26 U.S.C. 24(d)(3): "Paragraph (1) shall not apply to any taxpayer for any taxable year if such taxpayer elects to exclude any amount from gross income @@ -15,20 +15,22 @@ at the rates it would face if the excluded amount were added back (the Foreign Earned Income Tax Worksheet). The tax identity below is a property of the model's liability, not a worksheet result. + +Section 32(c)(1)(C) also denies the EITC to a section 911 claimant, in every +year, so the EITC of a filer with an exclusion is zero under both runs. + +The Hypothesis version of these checks is in +test_ctc_foreign_earned_income_exclusion_property.py, which skips without +the dev extra; the grid tests here always run. """ from functools import cache import numpy as np -import pytest from policyengine_core.reforms import Reform from policyengine_us import CountryTaxBenefitSystem, Simulation -# Hypothesis is a dev extra; skip rather than fail collection without it. -hypothesis = pytest.importorskip("hypothesis") -st = pytest.importorskip("hypothesis.strategies") - BAR = "gov.irs.credits.ctc.refundable.foreign_earned_income_exclusion_bar_applies" @@ -51,6 +53,7 @@ def no_bar_system(): "income_tax_capped_non_refundable_credits", # No residence test in the formula, so it is computed for every household. "pr_refundable_ctc", + "eitc", "income_tax", ] @@ -109,6 +112,9 @@ def assert_invariants(law, no_bar): assert (law["refundable_ctc"][excludes] == 0).all() # The Puerto Rico credit's social security route is barred as well. assert (law["pr_refundable_ctc"][excludes] == 0).all() + # Section 32(c)(1)(C): no EITC for a section 911 claimant, bar or not. + assert (law["eitc"][excludes] == 0).all() + assert (no_bar["eitc"][excludes] == 0).all() # Filers without the exclusion are untouched, bit for bit. for v in [ "refundable_ctc", @@ -187,31 +193,3 @@ def test_bar_applies_from_2015_but_not_in_2021(): law, no_bar = check(GRID, 2015) excludes = law["foreign_earned_income_exclusion"] > 0 assert (excludes & (no_bar["refundable_ctc"] > 0)).any() - - -household_strategy = st.fixed_dictionaries( - { - "married": st.booleans(), - # Age 17 at most: the model treats an unmarried filer's 18-year-old - # as a spouse. - "dependent_ages": st.lists(st.integers(0, 17), max_size=4), - "wages": st.integers(0, 500_000), - "exclusion": st.one_of(st.just(0), st.integers(1, 130_000)), - } -) - - -@hypothesis.settings( - max_examples=10, - deadline=None, - suppress_health_check=[ - hypothesis.HealthCheck.too_slow, - hypothesis.HealthCheck.data_too_large, - ], -) -@hypothesis.given( - st.lists(household_strategy, min_size=1, max_size=25), - st.sampled_from([2018, 2022, 2025, 2026]), -) -def test_random_households(households, year): - check(households, year) diff --git a/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion_property.py b/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion_property.py new file mode 100644 index 00000000000..dfc154f04c3 --- /dev/null +++ b/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion_property.py @@ -0,0 +1,39 @@ +"""Hypothesis checks for the section 911 bar on the refundable Child Tax Credit. + +The invariants and helpers are in test_ctc_foreign_earned_income_exclusion.py. +Hypothesis is a dev extra, so this module skips without it. +""" + +import pytest + +from test_ctc_foreign_earned_income_exclusion import check + +hypothesis = pytest.importorskip("hypothesis") +st = pytest.importorskip("hypothesis.strategies") + +household_strategy = st.fixed_dictionaries( + { + "married": st.booleans(), + # Age 17 at most: the model treats an unmarried filer's 18-year-old + # as a spouse. + "dependent_ages": st.lists(st.integers(0, 17), max_size=4), + "wages": st.integers(0, 500_000), + "exclusion": st.one_of(st.just(0), st.integers(1, 130_000)), + } +) + + +@hypothesis.settings( + max_examples=10, + deadline=None, + suppress_health_check=[ + hypothesis.HealthCheck.too_slow, + hypothesis.HealthCheck.data_too_large, + ], +) +@hypothesis.given( + st.lists(household_strategy, min_size=1, max_size=25), + st.sampled_from([2018, 2022, 2025, 2026]), +) +def test_random_households(households, year): + check(households, year) diff --git a/policyengine_us/variables/gov/irs/credits/earned_income/eligibility/eitc_eligible.py b/policyengine_us/variables/gov/irs/credits/earned_income/eligibility/eitc_eligible.py index cf22ac0d4b5..be4d58a42b0 100644 --- a/policyengine_us/variables/gov/irs/credits/earned_income/eligibility/eitc_eligible.py +++ b/policyengine_us/variables/gov/irs/credits/earned_income/eligibility/eitc_eligible.py @@ -6,7 +6,11 @@ class eitc_eligible(Variable): entity = TaxUnit label = "Eligible for EITC" definition_period = YEAR - reference = "https://www.law.cornell.edu/uscode/text/26/32#c_1_A" + reference = ( + "https://www.law.cornell.edu/uscode/text/26/32#c_1_A", + # No EITC for an individual who claims the benefits of section 911. + "https://www.law.cornell.edu/uscode/text/26/32#c_1_C", + ) def formula(tax_unit, period, parameters): eitc = parameters.gov.irs.credits.eitc(period) @@ -16,8 +20,16 @@ def formula(tax_unit, period, parameters): filers_has_ssn = tax_unit( "filer_meets_eitc_identification_requirements", period ) + # Section 32(c)(1)(C): "eligible individual" does not include an + # individual who claims the benefits of section 911 (Form 2555). + claims_section_911 = tax_unit("foreign_earned_income_exclusion", period) > 0 # Define eligibility before considering separate filer limitation. - eligible = demographic_eligible & investment_income_eligible & filers_has_ssn + eligible = ( + demographic_eligible + & investment_income_eligible + & filers_has_ssn + & ~claims_section_911 + ) # This parameter is true if separate filers are eligible. if eitc.eligibility.separate_filer: return eligible From 7c4f01a04fc9a4312c8038e3a05838cc500b0f26 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 10:16:49 -0400 Subject: [PATCH 06/12] Keep section 911 filers out of New Jersey's EITC and Colorado's CTC Fourth review of this PR found two state credits that the federal changes made worse for section 911 filers. - New Jersey: with the federal EITC now denied (32(c)(1)(C)), a childless section 911 filer fell into nj_eitc's age-exception fallback, which pays 40% of the federal childless maximum. A single 40-year-old with $5,000 of included wages and a $30,000 exclusion went from $153 to $259.60; the law gives $0. P.L. 2021, c.130, section 1a(4) requires the filer to "meet all qualifications, except for the minimum or maximum age, for the federal earned income tax credit". nj_childless_eitc_age_eligible now excludes section 911 claimants. - Colorado: co_refundable_ctc replicates the federal refundable CTC and subtracts the federal EITC from social security taxes. With that EITC now zero, a 2022 filer with three young children, $1,000 of included wages and a $30,000 exclusion got $76.50 of refundable basis. Colorado matches the allowed federal credit (1 CCR 201-2, Rule 39-22-129(1)), so the section 24(d)(3) bar now applies to the replica. Removing either guard fails exactly the new cases that assert it. Co-Authored-By: Claude Opus 5.5 --- .../ctc/federal_ctc/co_refundable_ctc.yaml | 33 ++++++++++ .../eitc/nj_childless_eitc_age_eligible.yaml | 23 +++++++ .../nj/tax/income/credits/eitc/nj_eitc.yaml | 63 +++++++++++++++++++ .../ctc/federal_ctc/co_refundable_ctc.py | 11 +++- .../eitc/nj_childless_eitc_age_eligible.py | 11 +++- 5 files changed, 139 insertions(+), 2 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/credits/ctc/federal_ctc/co_refundable_ctc.yaml b/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/credits/ctc/federal_ctc/co_refundable_ctc.yaml index 3aa9f41d41c..e64b6ddba6f 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/credits/ctc/federal_ctc/co_refundable_ctc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/co/tax/income/credits/ctc/federal_ctc/co_refundable_ctc.yaml @@ -127,3 +127,36 @@ eitc: 1_000 output: co_refundable_ctc: 9_500 + +# 1 CCR 201-2, Rule 39-22-129(1): the Colorado credit is a percentage of the +# allowed federal credit, so the section 24(d)(3) bar on section 911 filers +# carries over to the refundable part Colorado replicates. +- name: Section 911 filer gets no refundable part from social security taxes + period: 2022 + input: + state_code: CO + co_non_refundable_ctc: 0 + co_federal_ctc_maximum: 6_000 + co_ctc_eligible_children_count: 3 + tax_unit_earned_income: 1_000 + ctc_social_security_tax: 76.5 + eitc: 0 + foreign_earned_income_exclusion: 30_000 + output: + co_refundable_ctc: 0 + +- name: The same filer without a section 911 exclusion gets the social security tax excess + period: 2022 + input: + state_code: CO + co_non_refundable_ctc: 0 + co_federal_ctc_maximum: 6_000 + co_ctc_eligible_children_count: 3 + tax_unit_earned_income: 1_000 + ctc_social_security_tax: 76.5 + eitc: 0 + foreign_earned_income_exclusion: 0 + output: + # Earnings are under the $2,500 threshold, so line 19 is the $76.50 of + # social security taxes less the EITC; line 20 = min($4,500, $76.50). + co_refundable_ctc: 76.5 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nj/tax/income/credits/eitc/nj_childless_eitc_age_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/nj/tax/income/credits/eitc/nj_childless_eitc_age_eligible.yaml index 5759df32b3a..a94a5ac6133 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nj/tax/income/credits/eitc/nj_childless_eitc_age_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nj/tax/income/credits/eitc/nj_childless_eitc_age_eligible.yaml @@ -110,3 +110,26 @@ nj_eitc_income_eligible: true output: nj_childless_eitc_age_eligible: true + +# P.L. 2021, c.130, section 1a(4): the filer "shall meet all qualifications, +# except for the minimum or maximum age, for the federal earned income tax +# credit", including 26 U.S.C. 32(c)(1)(C) (no credit for an individual who +# claims the benefits of section 911). +- name: Section 911 claimant fails a non-age federal qualification. + period: 2025 + absolute_error_margin: 0 + input: + people: + person1: + age: 20 + households: + household: + members: [person1] + state_code: NJ + tax_units: + tax_unit: + members: [person1] + nj_eitc_income_eligible: true + foreign_earned_income_exclusion: 30_000 + output: + nj_childless_eitc_age_eligible: false diff --git a/policyengine_us/tests/policy/baseline/gov/states/nj/tax/income/credits/eitc/nj_eitc.yaml b/policyengine_us/tests/policy/baseline/gov/states/nj/tax/income/credits/eitc/nj_eitc.yaml index a72b9eed822..be6996b945b 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nj/tax/income/credits/eitc/nj_eitc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nj/tax/income/credits/eitc/nj_eitc.yaml @@ -75,3 +75,66 @@ nj_childless_eitc_age_eligible: true output: nj_eitc: 240 + +# Section 911 claimants: federal EITC eligibility fails under 26 U.S.C. +# 32(c)(1)(C), and the age exception in P.L. 2021, c.130, section 1a(4) +# requires every other federal qualification, so New Jersey pays nothing. +- name: Section 911 claimant within the federal age range gets no New Jersey EITC. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 5_000 + tax_units: + tax_unit: + members: [person1] + foreign_earned_income_exclusion: 30_000 + households: + household: + members: [person1] + state_code: NJ + output: + eitc: 0 + nj_eitc: 0 + +- name: The same filer without a section 911 exclusion gets 40% of the federal EITC. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 5_000 + tax_units: + tax_unit: + members: [person1] + foreign_earned_income_exclusion: 0 + households: + household: + members: [person1] + state_code: NJ + output: + # 7.65% of $5,000, below the $649 maximum and the phase-out. + eitc: 382.50 + nj_eitc: 153 + +- name: Section 911 claimant below the federal minimum age gets no New Jersey EITC. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 20 + employment_income: 5_000 + tax_units: + tax_unit: + members: [person1] + foreign_earned_income_exclusion: 30_000 + households: + household: + members: [person1] + state_code: NJ + output: + nj_eitc: 0 diff --git a/policyengine_us/variables/gov/states/co/tax/income/credits/ctc/federal_ctc/co_refundable_ctc.py b/policyengine_us/variables/gov/states/co/tax/income/credits/ctc/federal_ctc/co_refundable_ctc.py index 94428d1fa5a..686cb83972e 100644 --- a/policyengine_us/variables/gov/states/co/tax/income/credits/ctc/federal_ctc/co_refundable_ctc.py +++ b/policyengine_us/variables/gov/states/co/tax/income/credits/ctc/federal_ctc/co_refundable_ctc.py @@ -15,6 +15,11 @@ class co_refundable_ctc(Variable): "https://tax.colorado.gov/sites/tax/files/documents/DR_0104CN_2022.pdf#page=1", # Colorado Individual Income Tax Filing Guide - Instructions for Select Credits from the DR 0104CR - Line 1 Child Tax Credit "https://tax.colorado.gov/sites/tax/files/documents/DR_104_Book_2022.pdf#page=16", + # 1 CCR 201-2, Rule 39-22-129(1): the Colorado credit is a percentage + # of the allowed federal child tax credit. + "https://www.sos.state.co.us/CCR/GenerateRulePdf.do?fileName=1+CCR+201-2&ruleVersionId=10063#page=47", + # No refundable federal CTC for filers electing a section 911 exclusion. + "https://www.law.cornell.edu/uscode/text/26/24#d_3", ) defined_for = StateCode.CO @@ -46,4 +51,8 @@ def formula(tax_unit, period, parameters): relevant_earnings, max_(relevant_earnings, social_security_excess), ) # Line 19 - return min_(total_statutory_cap, tax_increase) # Line 20 + refundable_amount = min_(total_statutory_cap, tax_increase) # Line 20 + # Colorado matches the allowed federal credit, so the federal section + # 24(d)(3) bar on section 911 filers carries over. + barred = tax_unit("refundable_ctc_barred_by_section_911_exclusion", period) + return where(barred, 0, refundable_amount) diff --git a/policyengine_us/variables/gov/states/nj/tax/income/credits/eitc/nj_childless_eitc_age_eligible.py b/policyengine_us/variables/gov/states/nj/tax/income/credits/eitc/nj_childless_eitc_age_eligible.py index 810e72154e7..0dd34bc357e 100644 --- a/policyengine_us/variables/gov/states/nj/tax/income/credits/eitc/nj_childless_eitc_age_eligible.py +++ b/policyengine_us/variables/gov/states/nj/tax/income/credits/eitc/nj_childless_eitc_age_eligible.py @@ -7,7 +7,11 @@ class nj_childless_eitc_age_eligible(Variable): label = "New Jersey Eligible for EITC" definition_period = YEAR reference = ( - "https://law.justia.com/codes/new-jersey/2022/title-54a/section-54a-4-7/" + "https://law.justia.com/codes/new-jersey/2022/title-54a/section-54a-4-7/", + # P.L. 2021, c.130, section 1a(4): the filer "shall meet all + # qualifications, except for the minimum or maximum age, for the + # federal earned income tax credit". + "https://pub.njleg.state.nj.us/Bills/2020/PL21/130_.HTM", ) defined_for = StateCode.NJ @@ -27,9 +31,14 @@ def formula(tax_unit, period, parameters): age = person("age", period) age_eligible = age >= p.eligibility.age.min + # Section 32(c)(1)(C): a filer who claims the benefits of section 911 + # fails a federal qualification other than age. + claims_section_911 = tax_unit("foreign_earned_income_exclusion", period) > 0 + return ( ~separate & no_qualifying_children & tax_unit.any(age_eligible) & tax_unit("nj_eitc_income_eligible", period) + & ~claims_section_911 ) From 20cbba0ee95314e7dd8607af2d2c478e6c03f481 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 12:21:14 -0400 Subject: [PATCH 07/12] Section 911 claimants file, and state EITC recomputations exclude them Fifth review of this PR found three more places where the earlier changes moved a section 911 claimant the wrong way. - Filing: with the refundable CTC and the EITC denied, a claimant below the filing threshold lost the refundable-credit reason to file and became a model non-filer (no premium tax credit; Medicaid non-filer rules). But the exclusion is claimed on Form 2555 attached to the return (Treas. Reg. 1.911-7(a)(1): the election "must be filed either with the income tax return, or with an amended return"). tax_unit_is_filer now treats a positive exclusion as filing. - State EITCs: counting the exclusion toward the filing requirement (6012(c)) opened state credits that recompute a federal-style EITC without 32(c)(1)(C): Colorado's ITIN and under-25 paths, DC's childless credit, Illinois, Indiana's conformity check and Washington's WFTC. Each waives only named federal rules (age, SSN, separate filing). A new helper, eitc_section_911_eligible, applies the exclusion in calculate_eitc_like_amount and in DC's and Washington's own paths. - AFA: the reform's copy of refundable_ctc runs current law before 2025 and lacked the section 24(d)(3) bar, so a 2024 claimant gained $76.50 once the federal EITC went to zero. Removing any of the three guards fails exactly the new cases for it. Co-Authored-By: Claude Opus 5.5 --- .../afa/afa_other_dependent_credit.py | 6 +- .../gov/irs/tax_unit/tax_unit_is_filer.yaml | 17 +++ .../gov/states/section_911_state_eitc.yaml | 126 ++++++++++++++++++ .../afa/afa_other_dependent_credit.yaml | 31 +++++ policyengine_us/tools/state_eitc_helpers.py | 12 ++ .../variables/gov/irs/tax_unit_is_filer.py | 15 ++- .../eitc/dc_eitc_without_qualifying_child.py | 2 + .../credits/wa_working_families_tax_credit.py | 5 +- ...ilies_tax_credit_age_expansion_eligible.py | 2 + 9 files changed, 212 insertions(+), 4 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/states/section_911_state_eitc.yaml diff --git a/policyengine_us/reforms/congress/afa/afa_other_dependent_credit.py b/policyengine_us/reforms/congress/afa/afa_other_dependent_credit.py index 225697a4d0d..cd160364fcc 100644 --- a/policyengine_us/reforms/congress/afa/afa_other_dependent_credit.py +++ b/policyengine_us/reforms/congress/afa/afa_other_dependent_credit.py @@ -152,7 +152,11 @@ def formula(tax_unit, period, parameters): ctc_capped_by_tax = min_(total_ctc, limiting_tax) ctc_capped_by_increased_tax = min_(total_ctc, limiting_tax + phase_in) amount_ctc_would_increase = ctc_capped_by_increased_tax - ctc_capped_by_tax - return min_(maximum_refundable_ctc, amount_ctc_would_increase) + refundable_amount = min_(maximum_refundable_ctc, amount_ctc_would_increase) + # Before the credit is fully refundable (years the reform does not + # yet cover), current law applies, including section 24(d)(3). + barred = tax_unit("refundable_ctc_barred_by_section_911_exclusion", period) + return where(barred, 0, refundable_amount) class ctc(Variable): value_type = float diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax_unit/tax_unit_is_filer.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax_unit/tax_unit_is_filer.yaml index f55fec750d5..87929e76cfc 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax_unit/tax_unit_is_filer.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax_unit/tax_unit_is_filer.yaml @@ -102,3 +102,20 @@ output: # $10,000 is under the $15,750 threshold, and none of it is unearned. tax_unit_is_required_to_file: false + +# Treas. Reg. 1.911-7(a)(1): the section 911 election is filed with the return, +# and Form 2555 is attached to Form 1040 when filed, so a tax unit with an +# exclusion files even below the filing threshold and with no credit to claim. +- name: Section 911 claimant below the filing threshold still files + period: 2025 + input: + filing_status: SINGLE + age_head: 40 + employment_income: 5_000 + foreign_earned_income_exclusion: 1_000 + would_file_if_eligible_for_refundable_credit: false + would_file_taxes_voluntarily: false + output: + # $6,000 of gross income under section 6012(c), under $15,750. + tax_unit_is_required_to_file: false + tax_unit_is_filer: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/section_911_state_eitc.yaml b/policyengine_us/tests/policy/baseline/gov/states/section_911_state_eitc.yaml new file mode 100644 index 00000000000..4bec3bf257c --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/section_911_state_eitc.yaml @@ -0,0 +1,126 @@ +# 26 U.S.C. 32(c)(1)(C): no EITC for an individual who claims the benefits of +# section 911. These state credits are the federal credit with named rules +# waived (age, SSN, separate filing), so the section 911 bar still applies: +# Colorado (1 CCR 201-2, Rule 39-22-123.5(2)), DC (D.C. Code 47-1806.04(f)), +# Illinois (35 ILCS 5/212(a), (b-5), (b-10), (b-15)), Indiana (IRC conformity, +# IC 6-3-1-11) and Washington (WAC 458-20-285). +# +# Each household: single, 40, no children, $5,000 of included wages, a $30,000 +# section 911 exclusion, valid SSN, and no propensity to file otherwise. The +# exclusion makes filing required (26 U.S.C. 6012(c)), which must not open +# the state credit. + +- name: Section 911 claimant in CO gets no state EITC through the federal-style recomputation. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 5_000 + tax_units: + tax_unit: + members: [person1] + foreign_earned_income_exclusion: 30_000 + would_file_if_eligible_for_refundable_credit: false + would_file_taxes_voluntarily: false + households: + household: + members: [person1] + state_code: CO + output: + tax_unit_is_required_to_file: true + eitc: 0 + co_eitc: 0 + +- name: Section 911 claimant in DC gets no state EITC through the federal-style recomputation. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 5_000 + tax_units: + tax_unit: + members: [person1] + foreign_earned_income_exclusion: 30_000 + would_file_if_eligible_for_refundable_credit: false + would_file_taxes_voluntarily: false + households: + household: + members: [person1] + state_code: DC + output: + tax_unit_is_required_to_file: true + eitc: 0 + dc_eitc_without_qualifying_child: 0 + +- name: Section 911 claimant in IL gets no state EITC through the federal-style recomputation. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 5_000 + tax_units: + tax_unit: + members: [person1] + foreign_earned_income_exclusion: 30_000 + would_file_if_eligible_for_refundable_credit: false + would_file_taxes_voluntarily: false + households: + household: + members: [person1] + state_code: IL + output: + tax_unit_is_required_to_file: true + eitc: 0 + il_eitc: 0 + +- name: Section 911 claimant in WA gets no state EITC through the federal-style recomputation. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 5_000 + tax_units: + tax_unit: + members: [person1] + foreign_earned_income_exclusion: 30_000 + would_file_if_eligible_for_refundable_credit: false + would_file_taxes_voluntarily: false + households: + household: + members: [person1] + state_code: WA + output: + tax_unit_is_required_to_file: true + eitc: 0 + wa_working_families_tax_credit: 0 + +- name: Section 911 claimant in IN gets no state EITC through the federal-style recomputation. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + employment_income: 5_000 + tax_units: + tax_unit: + members: [person1] + foreign_earned_income_exclusion: 30_000 + would_file_if_eligible_for_refundable_credit: false + would_file_taxes_voluntarily: false + households: + household: + members: [person1] + state_code: IN + output: + tax_unit_is_required_to_file: true + eitc: 0 + in_eitc_eligible: false diff --git a/policyengine_us/tests/policy/contrib/congress/afa/afa_other_dependent_credit.yaml b/policyengine_us/tests/policy/contrib/congress/afa/afa_other_dependent_credit.yaml index 158bcec12dc..281ac74b739 100644 --- a/policyengine_us/tests/policy/contrib/congress/afa/afa_other_dependent_credit.yaml +++ b/policyengine_us/tests/policy/contrib/congress/afa/afa_other_dependent_credit.yaml @@ -289,3 +289,34 @@ ctc_lower_phase_out: 0 ctc_refundable_maximum: 9_960 ctc: 9_960 + +# Before the bill's fully refundable credit starts (2025), current law applies, +# including the section 24(d)(3) bar on section 911 filers. Without the bar, the +# zero EITC of a section 911 claimant (26 U.S.C. 32(c)(1)(C)) would leave $76.50 +# of social security taxes to refund. +- name: Section 911 filer gets no refundable CTC in 2024 with the reform active + period: 2024 + reforms: policyengine_us.reforms.congress.afa.afa_other_dependent_credit.afa_other_dependent_credit + input: + gov.contrib.congress.afa.in_effect: true + people: + head: + age: 40 + employment_income: 1_000 + child1: + age: 6 + child2: + age: 7 + child3: + age: 8 + tax_units: + tax_unit: + members: [head, child1, child2, child3] + foreign_earned_income_exclusion: 30_000 + households: + household: + members: [head, child1, child2, child3] + state_code: TX + output: + eitc: 0 + refundable_ctc: 0 diff --git a/policyengine_us/tools/state_eitc_helpers.py b/policyengine_us/tools/state_eitc_helpers.py index 02f891b989c..caa6ad87f5f 100644 --- a/policyengine_us/tools/state_eitc_helpers.py +++ b/policyengine_us/tools/state_eitc_helpers.py @@ -14,6 +14,16 @@ def eitc_filing_requirement_met(tax_unit, period): return is_required | files_voluntarily | would_file_for_credits +def eitc_section_911_eligible(tax_unit, period): + """26 U.S.C. 32(c)(1)(C): no EITC for anyone claiming section 911 benefits. + + State credits that recompute a federal-style EITC waive only the federal + rules they name (age, SSN, separate filing), so this one still applies. + """ + + return tax_unit("foreign_earned_income_exclusion", period) <= 0 + + def eitc_filing_status_eligible( tax_unit, period, parameters, separate_filer_eligible=None ): @@ -127,6 +137,7 @@ def calculate_eitc_like_amount( ) is_filer = eitc_filing_requirement_met(tax_unit, period) takes_up_eitc = tax_unit("takes_up_eitc", period) + section_911_eligible = eitc_section_911_eligible(tax_unit, period) return ( calculate_eitc_amount_from_parameters( tax_unit, period, eitc_parameters, child_count @@ -135,6 +146,7 @@ def calculate_eitc_like_amount( * filer_identification_eligible * investment_income_eligible * filing_status_eligible + * section_911_eligible * is_filer * takes_up_eitc ) diff --git a/policyengine_us/variables/gov/irs/tax_unit_is_filer.py b/policyengine_us/variables/gov/irs/tax_unit_is_filer.py index 8b50bfae600..5c5c731d40f 100644 --- a/policyengine_us/variables/gov/irs/tax_unit_is_filer.py +++ b/policyengine_us/variables/gov/irs/tax_unit_is_filer.py @@ -19,7 +19,13 @@ class tax_unit_is_filer(Variable): construction. """ definition_period = YEAR - reference = "https://www.law.cornell.edu/uscode/text/26/6012" + reference = ( + "https://www.law.cornell.edu/uscode/text/26/6012", + # A section 911 election is filed with the return. + "https://www.law.cornell.edu/cfr/text/26/1.911-7", + # "Attach Form 2555 to Form 1040 or 1040-SR when filed." + "https://www.irs.gov/pub/irs-pdf/i2555.pdf#page=2", + ) def formula(tax_unit, period, parameters): # Required to file based on income thresholds @@ -35,4 +41,9 @@ def formula(tax_unit, period, parameters): # Would file voluntarily for other reasons files_voluntarily = tax_unit("would_file_taxes_voluntarily", period) - return required | files_for_credits | files_voluntarily + # A section 911 exclusion is claimed on Form 2555 attached to the + # return (Treas. Reg. 1.911-7(a)(1); Form 2555 instructions), so a + # tax unit with an exclusion files whatever its other reasons. + claims_section_911 = tax_unit("foreign_earned_income_exclusion", period) > 0 + + return required | files_for_credits | files_voluntarily | claims_section_911 diff --git a/policyengine_us/variables/gov/states/dc/tax/income/credits/eitc/dc_eitc_without_qualifying_child.py b/policyengine_us/variables/gov/states/dc/tax/income/credits/eitc/dc_eitc_without_qualifying_child.py index 324b243f10f..964785d1302 100644 --- a/policyengine_us/variables/gov/states/dc/tax/income/credits/eitc/dc_eitc_without_qualifying_child.py +++ b/policyengine_us/variables/gov/states/dc/tax/income/credits/eitc/dc_eitc_without_qualifying_child.py @@ -2,6 +2,7 @@ from policyengine_us.tools.state_eitc_helpers import ( eitc_filing_requirement_met, eitc_filing_status_eligible, + eitc_section_911_eligible, ) @@ -45,6 +46,7 @@ def formula(tax_unit, period, parameters): & tax_unit.any(age_eligible) & tax_unit("eitc_investment_income_eligible", period) & eitc_filing_status_eligible(tax_unit, period, parameters) + & eitc_section_911_eligible(tax_unit, period) & eitc_filing_requirement_met(tax_unit, period) & tax_unit("takes_up_eitc", period) ) diff --git a/policyengine_us/variables/gov/states/wa/tax/income/credits/wa_working_families_tax_credit.py b/policyengine_us/variables/gov/states/wa/tax/income/credits/wa_working_families_tax_credit.py index f36bdc3260b..531e50f4d75 100644 --- a/policyengine_us/variables/gov/states/wa/tax/income/credits/wa_working_families_tax_credit.py +++ b/policyengine_us/variables/gov/states/wa/tax/income/credits/wa_working_families_tax_credit.py @@ -3,6 +3,7 @@ calculate_eitc_demographic_eligibility, eitc_filing_requirement_met, eitc_filing_status_eligible, + eitc_section_911_eligible, ) @@ -131,7 +132,9 @@ def formula(tax_unit, period, parameters): "wa_working_families_tax_credit_age_expansion_eligible", period ) - eligible = eitc_eligible | state_only_eitc_eligible | age_expansion_eligible + eligible = ( + eitc_eligible | state_only_eitc_eligible | age_expansion_eligible + ) & eitc_section_911_eligible(tax_unit, period) # Parameters are based on EITC-eligible children. # WFTC child count is the larger of the federally-counted children diff --git a/policyengine_us/variables/gov/states/wa/tax/income/credits/wa_working_families_tax_credit_age_expansion_eligible.py b/policyengine_us/variables/gov/states/wa/tax/income/credits/wa_working_families_tax_credit_age_expansion_eligible.py index e583a27bd21..d890a4c84b2 100644 --- a/policyengine_us/variables/gov/states/wa/tax/income/credits/wa_working_families_tax_credit_age_expansion_eligible.py +++ b/policyengine_us/variables/gov/states/wa/tax/income/credits/wa_working_families_tax_credit_age_expansion_eligible.py @@ -1,6 +1,7 @@ from policyengine_us.model_api import * from policyengine_us.tools.state_eitc_helpers import ( eitc_filing_requirement_met, + eitc_section_911_eligible, ) @@ -77,6 +78,7 @@ def formula(tax_unit, period, parameters): & income_eligible & investment_income_eligible & filers_have_tin + & eitc_section_911_eligible(tax_unit, period) & is_filer & takes_up_eitc ) From 050fb01b21260c8ed3453510a28b9dc1d565aaa3 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 14:45:47 -0400 Subject: [PATCH 08/12] Section 911 claimants file by propensity; AFA bar only before 2025 A continuing section 911 election needs no return below the filing threshold (Treas. Reg. 1.911-7(a)(1)), so a claimant under the threshold files only when the tax unit would file to claim a tax benefit. The American Family Act has no section 911 bar, so the reform's copy of the refundable CTC applies the bar only to years before 2025. Co-Authored-By: Claude Opus 5.5 --- .../afa/afa_other_dependent_credit.py | 11 ++- .../gov/irs/tax_unit/tax_unit_is_filer.yaml | 70 ++++++++++++++++++- .../afa/afa_other_dependent_credit.yaml | 27 +++++++ .../variables/gov/irs/tax_unit_is_filer.py | 23 ++++-- 4 files changed, 121 insertions(+), 10 deletions(-) diff --git a/policyengine_us/reforms/congress/afa/afa_other_dependent_credit.py b/policyengine_us/reforms/congress/afa/afa_other_dependent_credit.py index cd160364fcc..0c6f0216231 100644 --- a/policyengine_us/reforms/congress/afa/afa_other_dependent_credit.py +++ b/policyengine_us/reforms/congress/afa/afa_other_dependent_credit.py @@ -3,6 +3,10 @@ from policyengine_core.periods import instant import numpy as np +# American Family Act of 2025, section 2(f)(1): the amendments apply to +# taxable years beginning after December 31, 2024. +AFA_FIRST_TAXABLE_YEAR = 2025 + def create_afa_other_dependent_credit() -> Reform: class other_dependent_credit(Variable): @@ -153,8 +157,11 @@ def formula(tax_unit, period, parameters): ctc_capped_by_increased_tax = min_(total_ctc, limiting_tax + phase_in) amount_ctc_would_increase = ctc_capped_by_increased_tax - ctc_capped_by_tax refundable_amount = min_(maximum_refundable_ctc, amount_ctc_would_increase) - # Before the credit is fully refundable (years the reform does not - # yet cover), current law applies, including section 24(d)(3). + # The bill replaces section 24 for taxable years beginning after + # 2024 and has no section 911 bar. Earlier years keep current law, + # including section 24(d)(3). + if period.start.year >= AFA_FIRST_TAXABLE_YEAR: + return refundable_amount barred = tax_unit("refundable_ctc_barred_by_section_911_exclusion", period) return where(barred, 0, refundable_amount) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax_unit/tax_unit_is_filer.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax_unit/tax_unit_is_filer.yaml index 87929e76cfc..47d5c22d186 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax_unit/tax_unit_is_filer.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax_unit/tax_unit_is_filer.yaml @@ -106,7 +106,21 @@ # Treas. Reg. 1.911-7(a)(1): the section 911 election is filed with the return, # and Form 2555 is attached to Form 1040 when filed, so a tax unit with an # exclusion files even below the filing threshold and with no credit to claim. -- name: Section 911 claimant below the filing threshold still files +- name: Section 911 claimant below the filing threshold files to claim the exclusion + period: 2025 + input: + filing_status: SINGLE + age_head: 40 + employment_income: 5_000 + foreign_earned_income_exclusion: 1_000 + output: + # $6,000 of gross income under section 6012(c), under $15,750. Section + # 32(c)(1)(C) denies the EITC, so the exclusion is the reason to file. + tax_unit_is_required_to_file: false + eitc: 0 + tax_unit_is_filer: true + +- name: Section 911 claimant below the filing threshold who would not file does not period: 2025 input: filing_status: SINGLE @@ -116,6 +130,58 @@ would_file_if_eligible_for_refundable_credit: false would_file_taxes_voluntarily: false output: - # $6,000 of gross income under section 6012(c), under $15,750. + # A continuing election needs no return below the filing threshold + # (Treas. Reg. 1.911-7(a)(1)). + tax_unit_is_required_to_file: false + tax_unit_is_filer: false + +- name: Section 911 parents under the threshold keep filing without the refundable credits + period: 2025 + input: + people: + head: + age: 40 + employment_income: 28_000 + spouse: + age: 40 + child: + age: 5 + tax_units: + tax_unit: + members: [head, spouse, child] + foreign_earned_income_exclusion: 1_000 + households: + household: + members: [head, spouse, child] + state_code: TX + output: + # $29,000 of gross income under section 6012(c), under $31,500. tax_unit_is_required_to_file: false + refundable_ctc: 0 + eitc: 0 tax_unit_is_filer: true + +- name: Section 911 parents under the threshold who would not file do not + period: 2025 + input: + people: + head: + age: 40 + employment_income: 28_000 + spouse: + age: 40 + child: + age: 5 + tax_units: + tax_unit: + members: [head, spouse, child] + foreign_earned_income_exclusion: 1_000 + would_file_if_eligible_for_refundable_credit: false + would_file_taxes_voluntarily: false + households: + household: + members: [head, spouse, child] + state_code: TX + output: + tax_unit_is_required_to_file: false + tax_unit_is_filer: false diff --git a/policyengine_us/tests/policy/contrib/congress/afa/afa_other_dependent_credit.yaml b/policyengine_us/tests/policy/contrib/congress/afa/afa_other_dependent_credit.yaml index 281ac74b739..d567314cf78 100644 --- a/policyengine_us/tests/policy/contrib/congress/afa/afa_other_dependent_credit.yaml +++ b/policyengine_us/tests/policy/contrib/congress/afa/afa_other_dependent_credit.yaml @@ -320,3 +320,30 @@ output: eitc: 0 refundable_ctc: 0 + +# The bill has no section 911 bar and no sunset, so the bar applies only to +# years before 2025. The reform's parameter changes stop in 2039 (an existing +# limitation), which leaves 2040 on the partial-refund branch: $825 here, +# where the bill would pay the full credit. The bar must not zero it. +- name: Section 911 filer keeps the reform's refundable CTC after 2039 + period: 2040 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.afa.afa_other_dependent_credit.afa_other_dependent_credit + input: + gov.contrib.congress.afa.in_effect: true + people: + head: + age: 40 + employment_income: 8_000 + child1: + age: 5 + tax_units: + tax_unit: + members: [head, child1] + foreign_earned_income_exclusion: 1_000 + households: + household: + members: [head, child1] + state_code: TX + output: + refundable_ctc: 825 diff --git a/policyengine_us/variables/gov/irs/tax_unit_is_filer.py b/policyengine_us/variables/gov/irs/tax_unit_is_filer.py index 5c5c731d40f..8395e8c7b47 100644 --- a/policyengine_us/variables/gov/irs/tax_unit_is_filer.py +++ b/policyengine_us/variables/gov/irs/tax_unit_is_filer.py @@ -11,7 +11,9 @@ class tax_unit_is_filer(Variable): A tax unit files if any of the following apply: 1. They are legally required to file (IRC § 6012) 2. They are eligible for refundable credits and would file to claim them - 3. They would file voluntarily for other reasons (state requirements, + 3. They claim a section 911 exclusion and would file to claim a tax + benefit (the exclusion is claimed on Form 2555 with a return) + 4. They would file voluntarily for other reasons (state requirements, documentation, habit) The propensity variables (would_file_if_eligible_for_refundable_credit @@ -21,8 +23,13 @@ class tax_unit_is_filer(Variable): definition_period = YEAR reference = ( "https://www.law.cornell.edu/uscode/text/26/6012", - # A section 911 election is filed with the return. + # A section 911 election is filed with the return for its first year + # and then stays in effect. "https://www.law.cornell.edu/cfr/text/26/1.911-7", + # "in the case of an individual claiming an exclusion under section + # 911, he shall attach Form 2555 to the return required under this + # paragraph" + "https://www.law.cornell.edu/cfr/text/26/1.6012-1", # "Attach Form 2555 to Form 1040 or 1040-SR when filed." "https://www.irs.gov/pub/irs-pdf/i2555.pdf#page=2", ) @@ -41,9 +48,13 @@ def formula(tax_unit, period, parameters): # Would file voluntarily for other reasons files_voluntarily = tax_unit("would_file_taxes_voluntarily", period) - # A section 911 exclusion is claimed on Form 2555 attached to the - # return (Treas. Reg. 1.911-7(a)(1); Form 2555 instructions), so a - # tax unit with an exclusion files whatever its other reasons. + # A section 911 exclusion is claimed on Form 2555 attached to a + # return (Treas. Reg. 1.911-7(a)(1) and 1.6012-1(a)(3)). Sections + # 24(d)(3) and 32(c)(1)(C) deny the claimant the refundable credits, + # so those credits cannot be its reason to file. A claimant under the + # filing threshold need not file in a later year of a continuing + # election, so the propensity to file for a tax benefit decides. claims_section_911 = tax_unit("foreign_earned_income_exclusion", period) > 0 + files_for_section_911 = claims_section_911 & would_file_for_credits - return required | files_for_credits | files_voluntarily | claims_section_911 + return required | files_for_credits | files_for_section_911 | files_voluntarily From e1631f819973546262eb489bdb0c74ae6f944b2e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 05:06:48 -0400 Subject: [PATCH 09/12] Read the section 24(d)(3) bar in Credit Limit Worksheet B ctc_credit_limit_worksheet_b_applies (#9742) repeated the bar's test (exclusion > 0 and the parameter on, outside fully refundable years) instead of reading refundable_ctc_barred_by_section_911_exclusion. The two agree whenever both are calculated, so no output changes; they could disagree only when the bar is set as an input. Worksheet B now reads the bar, so one variable decides both the refund and the worksheet (2025 Schedule 8812 instructions: Worksheet B only if "You are not filing Form 2555"). Two YAML cases set the bar as an input in each direction, and the grid and Hypothesis invariants check that Worksheet B equals its bar-off value less the barred filers. Co-Authored-By: Claude Opus 5.5 --- .../ctc_credit_limit_worksheet_b_applies.yaml | 20 +++++++++++++++++++ ...est_ctc_foreign_earned_income_exclusion.py | 7 +++++++ .../ctc_credit_limit_worksheet_b_applies.py | 11 ++++------ ...ble_ctc_barred_by_section_911_exclusion.py | 3 ++- 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_credit_limit_worksheet_b_applies.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_credit_limit_worksheet_b_applies.yaml index 4b7b2ec6e97..aac331e1b63 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_credit_limit_worksheet_b_applies.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/refundable/ctc_credit_limit_worksheet_b_applies.yaml @@ -41,3 +41,23 @@ foreign_earned_income_exclusion: 10_000 output: ctc_credit_limit_worksheet_b_applies: true + +# Worksheet B reads the section 24(d)(3) bar rather than repeating its test, so +# a bar set as an input decides both the refund and the worksheet. +- name: Does not apply when the section 24(d)(3) bar is set as an input, even without an exclusion. + period: 2025 + input: + ctc_qualifying_children: 2 + foreign_earned_income_exclusion: 0 + refundable_ctc_barred_by_section_911_exclusion: true + output: + ctc_credit_limit_worksheet_b_applies: false + +- name: Applies to a Form 2555 filer when the section 24(d)(3) bar is set to false as an input. + period: 2025 + input: + ctc_qualifying_children: 2 + foreign_earned_income_exclusion: 10_000 + refundable_ctc_barred_by_section_911_exclusion: false + output: + ctc_credit_limit_worksheet_b_applies: true diff --git a/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py b/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py index 3b8b38a8feb..5e8efd58e93 100644 --- a/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py +++ b/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py @@ -48,6 +48,7 @@ def no_bar_system(): "ctc", "ctc_refundable_maximum", "refundable_ctc_barred_by_section_911_exclusion", + "ctc_credit_limit_worksheet_b_applies", "refundable_ctc", "non_refundable_ctc", "income_tax_capped_non_refundable_credits", @@ -115,6 +116,12 @@ def assert_invariants(law, no_bar): # Section 32(c)(1)(C): no EITC for a section 911 claimant, bar or not. assert (law["eitc"][excludes] == 0).all() assert (no_bar["eitc"][excludes] == 0).all() + # Schedule 8812: a filer the bar denies the refund skips Credit Limit + # Worksheet B. With the bar switched off, excluders complete it as other + # filers do, so the bar is its only Form 2555 test. + worksheet_b = "ctc_credit_limit_worksheet_b_applies" + assert not (law[worksheet_b] & law[barred]).any() + assert np.array_equal(law[worksheet_b], no_bar[worksheet_b] & ~law[barred]) # Filers without the exclusion are untouched, bit for bit. for v in [ "refundable_ctc", diff --git a/policyengine_us/variables/gov/irs/credits/ctc/refundable/ctc_credit_limit_worksheet_b_applies.py b/policyengine_us/variables/gov/irs/credits/ctc/refundable/ctc_credit_limit_worksheet_b_applies.py index 07af7bbaf36..533956cea34 100644 --- a/policyengine_us/variables/gov/irs/credits/ctc/refundable/ctc_credit_limit_worksheet_b_applies.py +++ b/policyengine_us/variables/gov/irs/credits/ctc/refundable/ctc_credit_limit_worksheet_b_applies.py @@ -31,10 +31,7 @@ def formula(tax_unit, period, parameters): # whose CTC was not fully refundable. return np.zeros(tax_unit.count, dtype=bool) has_qualifying_child = tax_unit("ctc_qualifying_children", period) > 0 - # Form 2555 filers skip Worksheet B from 2015, when the refundable CTC - # was first denied to them. - files_form_2555 = tax_unit("foreign_earned_income_exclusion", period) > 0 - excluded = ( - files_form_2555 & p.refundable.foreign_earned_income_exclusion_bar_applies - ) - return has_qualifying_child & ~excluded + # Form 2555 filers skip Worksheet B in the years section 24(d)(3) + # denies them the refundable CTC (from 2015). + barred = tax_unit("refundable_ctc_barred_by_section_911_exclusion", period) + return has_qualifying_child & ~barred diff --git a/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.py b/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.py index 1c48fa15c50..c460aabcf1c 100644 --- a/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.py +++ b/policyengine_us/variables/gov/irs/credits/ctc/refundable/refundable_ctc_barred_by_section_911_exclusion.py @@ -8,7 +8,8 @@ class refundable_ctc_barred_by_section_911_exclusion(Variable): documentation = ( "Whether section 24(d)(3) denies the refundable Child Tax Credit " "because the filer elects to exclude foreign earned income or housing " - "amounts from gross income under section 911 (Form 2555)." + "amounts from gross income under section 911 (Form 2555). These " + "filers also skip Schedule 8812 Credit Limit Worksheet B." ) definition_period = YEAR reference = ( From 7caa0a57332a349883110270243a8412203477d9 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 05:06:49 -0400 Subject: [PATCH 10/12] Update section 911(f) comments now that main stacks the tax Since #9761, main taxes a Form 2555 filer's included income at the rates it would face with the exclusion added back. The grid test's docstring and the integration.yaml note said the model did not, which contradicted the #9761 case at the top of the same file. Both now describe the stacked tax, and the grid test asserts that it reaches filers whose stacked tax leaves part of the credit to refund, so the tax identity is checked on stacked tax. The changelog fragment now names the state credits, the reforms and the new filing reason. Co-Authored-By: Claude Opus 5.5 --- ...e-foreign-earned-income-exclusion.fixed.md | 2 +- .../gov/irs/credits/ctc/integration.yaml | 14 ++++++-------- ...est_ctc_foreign_earned_income_exclusion.py | 19 +++++++++++++++---- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md b/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md index 787e7932afb..d2ee1857da9 100644 --- a/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md +++ b/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md @@ -1 +1 @@ -Deny the refundable Child Tax Credit to filers who elect to exclude foreign earned income or housing amounts under section 911 (26 U.S.C. 24(d)(3)) for tax years from 2015 except fully refundable 2021, including the Puerto Rico refundable Child Tax Credit and the Wyden-Smith CTC expansion reform; deny the EITC to filers who claim the benefits of section 911 (26 U.S.C. 32(c)(1)(C)); and count the section 911 exclusion in gross income for the filing requirement (26 U.S.C. 6012(c)). +Deny the refundable Child Tax Credit to filers who elect to exclude foreign earned income or housing amounts under section 911 (26 U.S.C. 24(d)(3)) for tax years from 2015 except fully refundable 2021, including the Puerto Rico refundable Child Tax Credit, Colorado's replica of the refundable credit, the Wyden-Smith CTC expansion reform and the American Family Act reform before 2025; deny the EITC to filers who claim the benefits of section 911 (26 U.S.C. 32(c)(1)(C)), including in New Jersey's childless EITC age exception and the Colorado, DC, Illinois, Indiana and Washington credits that recompute the federal EITC; count the section 911 exclusion in gross income for the filing requirement (26 U.S.C. 6012(c)); and treat a section 911 claimant below the filing threshold as a filer when the tax unit would file to claim a refundable credit. diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/integration.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/integration.yaml index af65d0db85a..f7dfd6947f2 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/integration.yaml @@ -532,14 +532,12 @@ ctc: 3_500 # 3000 for 10-year-old + 500 for 18-year-old (ODC) ctc_adult_individual_maximum: [0, 0, 500] # ODC for 18-year-old under ARPA -# The two cases below use a household with zero taxable income on purpose. -# The model does not apply the section 911(f) rule that taxes included income -# at the rates it would face with the excluded amount added back (Foreign -# Earned Income Tax Worksheet, Form 1040 line 16). That worksheet is skipped -# when taxable income is zero, so the tax and Schedule 8812 line 12, 14 and 27 -# figures here are the form's. non_refundable_ctc is the credit before the -# section 26(a) limit; the limited amount is -# income_tax_capped_non_refundable_credits (line 14 here: $0). +# The two cases below have zero taxable income, so the Foreign Earned Income +# Tax Worksheet (Form 1040 line 16), which applies section 911(f), is skipped +# and the tax is $0; the first case in this file has a Form 2555 filer with +# positive taxable income. non_refundable_ctc is the credit before the section +# 26(a) limit; the limited amount is income_tax_capped_non_refundable_credits +# (Schedule 8812 line 14, here $0). - name: Head of household with two children and a section 911 exclusion gets no additional child tax credit in 2025. period: 2025 absolute_error_margin: 0.01 diff --git a/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py b/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py index 5e8efd58e93..3b6156e640c 100644 --- a/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py +++ b/policyengine_us/tests/test_ctc_foreign_earned_income_exclusion.py @@ -11,10 +11,14 @@ Households live in Texas and take the standard deduction, so the CTC's tax-liability limit equals the tax the model computes. -The model does not apply the section 911(f) rule that taxes included income -at the rates it would face if the excluded amount were added back (the Foreign -Earned Income Tax Worksheet). The tax identity below is a property of the -model's liability, not a worksheet result. +Both runs tax an excluding filer's included income at the rates it would face +if the excluded amount were added back (section 911(f), the Foreign Earned +Income Tax Worksheet), so the tax identity below holds for that stacked tax. +The grid includes filers whose stacked tax leaves part of the credit to +refund. + +Schedule 8812 Credit Limit Worksheet B is not for Form 2555 filers, and it +reads the bar, so a barred filer never completes it. Section 32(c)(1)(C) also denies the EITC to a section 911 claimant, in every year, so the EITC of a filer with an exclusion is zero under both runs. @@ -184,6 +188,13 @@ def test_grid_invariants(): excludes = law["foreign_earned_income_exclusion"] > 0 # The grid reaches filers whose refund the bar removes. assert (excludes & (no_bar["refundable_ctc"] > 0)).any() + # Some of them have section 911(f) stacked tax above the tax on the same + # household without the exclusion, so the tax identity is checked on + # stacked tax. + twin = [GRID.index({**h, "exclusion": 0}) for h in GRID] + tax = law["income_tax_before_credits"] + stacked = tax > tax[twin] + 1 + assert (excludes & stacked & (no_bar["refundable_ctc"] > 0)).any() def test_bar_applies_from_2015_but_not_in_2021(): From e0b5bafd1588930328a8de213945fd19fc0f1d81 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 05:53:59 -0400 Subject: [PATCH 11/12] Describe the section 911 filing reason as a propensity in the changelog The filing reason reads would_file_if_eligible_for_refundable_credit; the claimant needs no refundable credit. The integration.yaml note now says the Foreign Earned Income Tax Worksheet is skipped for the Form 2555 filer (the second case has no exclusion) and why the capped credits equal Schedule 8812 line 14 there. Co-Authored-By: Claude Opus 5.5 --- ...ndable-foreign-earned-income-exclusion.fixed.md | 2 +- .../baseline/gov/irs/credits/ctc/integration.yaml | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md b/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md index d2ee1857da9..7bc1948aafd 100644 --- a/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md +++ b/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md @@ -1 +1 @@ -Deny the refundable Child Tax Credit to filers who elect to exclude foreign earned income or housing amounts under section 911 (26 U.S.C. 24(d)(3)) for tax years from 2015 except fully refundable 2021, including the Puerto Rico refundable Child Tax Credit, Colorado's replica of the refundable credit, the Wyden-Smith CTC expansion reform and the American Family Act reform before 2025; deny the EITC to filers who claim the benefits of section 911 (26 U.S.C. 32(c)(1)(C)), including in New Jersey's childless EITC age exception and the Colorado, DC, Illinois, Indiana and Washington credits that recompute the federal EITC; count the section 911 exclusion in gross income for the filing requirement (26 U.S.C. 6012(c)); and treat a section 911 claimant below the filing threshold as a filer when the tax unit would file to claim a refundable credit. +Deny the refundable Child Tax Credit to filers who elect to exclude foreign earned income or housing amounts under section 911 (26 U.S.C. 24(d)(3)) for tax years from 2015 except fully refundable 2021, including the Puerto Rico refundable Child Tax Credit, Colorado's replica of the refundable credit, the Wyden-Smith CTC expansion reform and the American Family Act reform before 2025; deny the EITC to filers who claim the benefits of section 911 (26 U.S.C. 32(c)(1)(C)), including in New Jersey's childless EITC age exception and the Colorado, DC, Illinois, Indiana and Washington credits that recompute the federal EITC; count the section 911 exclusion in gross income for the filing requirement (26 U.S.C. 6012(c)); and treat a section 911 claimant below the filing threshold as a filer when the tax unit would file if eligible for a refundable credit (would_file_if_eligible_for_refundable_credit). diff --git a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/integration.yaml b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/integration.yaml index f7dfd6947f2..0d6f23ecd1e 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/credits/ctc/integration.yaml @@ -532,12 +532,14 @@ ctc: 3_500 # 3000 for 10-year-old + 500 for 18-year-old (ODC) ctc_adult_individual_maximum: [0, 0, 500] # ODC for 18-year-old under ARPA -# The two cases below have zero taxable income, so the Foreign Earned Income -# Tax Worksheet (Form 1040 line 16), which applies section 911(f), is skipped -# and the tax is $0; the first case in this file has a Form 2555 filer with -# positive taxable income. non_refundable_ctc is the credit before the section -# 26(a) limit; the limited amount is income_tax_capped_non_refundable_credits -# (Schedule 8812 line 14, here $0). +# Both cases below have zero taxable income and $0 of tax before credits. For +# the Form 2555 filer, the Foreign Earned Income Tax Worksheet (Form 1040 line +# 16), which applies section 911(f), is skipped when taxable income is zero; +# the first case in this file has a Form 2555 filer with positive taxable +# income. +# non_refundable_ctc is the credit before the section 26(a) limit; the limited +# amount is income_tax_capped_non_refundable_credits, which equals Schedule +# 8812 line 14 here because the CTC is the only non-refundable credit ($0). - name: Head of household with two children and a section 911 exclusion gets no additional child tax credit in 2025. period: 2025 absolute_error_margin: 0.01 From 0df2fbec60b09f673fc3dec072efb0f1e7020176 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 7 Oct 2026 08:46:42 -0400 Subject: [PATCH 12/12] Preserve AFA Puerto Rico refundable CTC after 2039 --- ...e-foreign-earned-income-exclusion.fixed.md | 2 +- .../afa/afa_other_dependent_credit.py | 25 ++++--- .../refundable_ctc/pr_refundable_ctc.yaml | 22 +++++++ .../afa/afa_other_dependent_credit.yaml | 29 ++++++++ .../congress/afa/test_afa_section_911.py | 66 +++++++++++++++++++ 5 files changed, 134 insertions(+), 10 deletions(-) create mode 100644 policyengine_us/tests/policy/contrib/congress/afa/test_afa_section_911.py diff --git a/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md b/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md index 7bc1948aafd..f2a2d790c94 100644 --- a/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md +++ b/changelog.d/ctc-refundable-foreign-earned-income-exclusion.fixed.md @@ -1 +1 @@ -Deny the refundable Child Tax Credit to filers who elect to exclude foreign earned income or housing amounts under section 911 (26 U.S.C. 24(d)(3)) for tax years from 2015 except fully refundable 2021, including the Puerto Rico refundable Child Tax Credit, Colorado's replica of the refundable credit, the Wyden-Smith CTC expansion reform and the American Family Act reform before 2025; deny the EITC to filers who claim the benefits of section 911 (26 U.S.C. 32(c)(1)(C)), including in New Jersey's childless EITC age exception and the Colorado, DC, Illinois, Indiana and Washington credits that recompute the federal EITC; count the section 911 exclusion in gross income for the filing requirement (26 U.S.C. 6012(c)); and treat a section 911 claimant below the filing threshold as a filer when the tax unit would file if eligible for a refundable credit (would_file_if_eligible_for_refundable_credit). +Deny the refundable Child Tax Credit to filers who elect to exclude foreign earned income or housing amounts under section 911 (26 U.S.C. 24(d)(3)) for tax years from 2015 except fully refundable 2021, including the Puerto Rico refundable Child Tax Credit, Colorado's replica of the refundable credit, the Wyden-Smith CTC expansion reform and the American Family Act reform before 2025; deny the EITC to filers who claim the benefits of section 911 (26 U.S.C. 32(c)(1)(C)), including in New Jersey's childless EITC age exception and the Colorado, DC, Illinois, Indiana and Washington credits that recompute the federal EITC; count the section 911 exclusion in gross income for the filing requirement (26 U.S.C. 6012(c)); and treat a section 911 claimant below the filing threshold as a filer when the tax unit would file if eligible for a refundable credit (would_file_if_eligible_for_refundable_credit). Under the American Family Act, waive the shared section 911 refundable CTC bar from 2025 onward, including the Puerto Rico refund after 2039. diff --git a/policyengine_us/reforms/congress/afa/afa_other_dependent_credit.py b/policyengine_us/reforms/congress/afa/afa_other_dependent_credit.py index ae2c35e87ac..3b4e3719db4 100644 --- a/policyengine_us/reforms/congress/afa/afa_other_dependent_credit.py +++ b/policyengine_us/reforms/congress/afa/afa_other_dependent_credit.py @@ -3,12 +3,23 @@ from policyengine_core.periods import instant import numpy as np -# American Family Act of 2025, section 2(f)(1): the amendments apply to -# taxable years beginning after December 31, 2024. -AFA_FIRST_TAXABLE_YEAR = 2025 - def create_afa_other_dependent_credit() -> Reform: + class refundable_ctc_barred_by_section_911_exclusion(Variable): + reference = "https://www.bennet.senate.gov/wp-content/uploads/2025/04/American-Family-Act-2025.pdf" + documentation = ( + "The American Family Act replaces section 24 from 2025 with " + "section 24A, which does not bar refundability for section 911 " + "claimants, including in Puerto Rico. Earlier years retain " + "the current-law bar." + ) + + def formula_2025(tax_unit, period, parameters): + # Section 2(c) ends old section 24 after 2024; sections 24A(d) + # and 2(f)(1) provide refundability from 2025 without a sunset. + # Keep this rule after the fully_refundable parameter expires. + return False + class other_dependent_credit(Variable): value_type = float entity = TaxUnit @@ -157,11 +168,6 @@ def formula(tax_unit, period, parameters): ctc_capped_by_increased_tax = min_(total_ctc, limiting_tax + phase_in) amount_ctc_would_increase = ctc_capped_by_increased_tax - ctc_capped_by_tax refundable_amount = min_(maximum_refundable_ctc, amount_ctc_would_increase) - # The bill replaces section 24 for taxable years beginning after - # 2024 and has no section 911 bar. Earlier years keep current law, - # including section 24(d)(3). - if period.start.year >= AFA_FIRST_TAXABLE_YEAR: - return refundable_amount barred = tax_unit("refundable_ctc_barred_by_section_911_exclusion", period) return where(barred, 0, refundable_amount) @@ -267,6 +273,7 @@ def apply(self): self.update_variable(ctc_lower_phase_out) self.update_variable(ctc_higher_phase_out) self.update_variable(ctc) + self.update_variable(refundable_ctc_barred_by_section_911_exclusion) self.update_variable(refundable_ctc) self.update_variable(ctc_child_individual_maximum_arpa) self.modify_parameters(modify_parameters) diff --git a/policyengine_us/tests/policy/baseline/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc.yaml b/policyengine_us/tests/policy/baseline/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc.yaml index 0f66bd6ef35..9525cc2a5d0 100644 --- a/policyengine_us/tests/policy/baseline/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/territories/pr/tax/income/credits/refundable_ctc/pr_refundable_ctc.yaml @@ -60,3 +60,25 @@ foreign_earned_income_exclusion: 30_000 output: pr_refundable_ctc: 2_000 + +- name: Puerto Rico section 911 filer remains barred in 2040 under current law + period: 2040 + input: + people: + head: + age: 40 + employment_income: 8_000 + child1: + age: 5 + tax_units: + tax_unit: + members: [head, child1] + foreign_earned_income_exclusion: 1_000 + households: + household: + members: [head, child1] + state_code: PR + output: + refundable_ctc_barred_by_section_911_exclusion: true + refundable_ctc: 0 + pr_refundable_ctc: 0 diff --git a/policyengine_us/tests/policy/contrib/congress/afa/afa_other_dependent_credit.yaml b/policyengine_us/tests/policy/contrib/congress/afa/afa_other_dependent_credit.yaml index 7a557a60c45..32f9bb5f625 100644 --- a/policyengine_us/tests/policy/contrib/congress/afa/afa_other_dependent_credit.yaml +++ b/policyengine_us/tests/policy/contrib/congress/afa/afa_other_dependent_credit.yaml @@ -322,6 +322,7 @@ state_code: TX output: eitc: 0 + refundable_ctc_barred_by_section_911_exclusion: true refundable_ctc: 0 # The bill has no section 911 bar and no sunset, so the bar applies only to @@ -349,4 +350,32 @@ members: [head, child1] state_code: TX output: + refundable_ctc_barred_by_section_911_exclusion: false refundable_ctc: 825 + +# Section 24A(d) includes Puerto Rico. Its refund must survive even after +# the reform's fully_refundable parameter expires, just like the federal one. +- name: Puerto Rico section 911 filer keeps the AFA refund after 2039 + period: 2040 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.afa.afa_other_dependent_credit.afa_other_dependent_credit + input: + gov.contrib.congress.afa.in_effect: true + people: + head: + age: 40 + employment_income: 8_000 + child1: + age: 5 + tax_units: + tax_unit: + members: [head, child1] + foreign_earned_income_exclusion: 1_000 + households: + household: + members: [head, child1] + state_code: PR + output: + refundable_ctc_barred_by_section_911_exclusion: false + refundable_ctc: 825 + pr_refundable_ctc: 825 diff --git a/policyengine_us/tests/policy/contrib/congress/afa/test_afa_section_911.py b/policyengine_us/tests/policy/contrib/congress/afa/test_afa_section_911.py new file mode 100644 index 00000000000..f15abc8c569 --- /dev/null +++ b/policyengine_us/tests/policy/contrib/congress/afa/test_afa_section_911.py @@ -0,0 +1,66 @@ +"""Exercise section 911 through the production AFA activation path. + +The neighboring YAML policy tests apply structural reforms directly. This +integration test uses Reform.from_dict to verify that enabling AFA updates +the shared bar, retains its pre-2025 formula, and reaches the Puerto Rico +credit consumer after AFA's fully-refundable parameter expires in 2039. +""" + +import pytest +from policyengine_core.reforms import Reform + +from policyengine_us import Simulation + + +YEARS = (2024, 2025, 2039, 2040) +BAR = "refundable_ctc_barred_by_section_911_exclusion" +CREDITS = ("refundable_ctc", "pr_refundable_ctc") + + +def test_afa_activation_preserves_section_911_rules_across_years(): + members = ["parent", "child"] + situation = { + "people": { + "parent": { + "age": {str(year): 40 for year in YEARS}, + "employment_income": {str(year): 8_000 for year in YEARS}, + }, + "child": {"age": {str(year): 5 for year in YEARS}}, + }, + "tax_units": { + "tax_unit": { + "members": members, + "foreign_earned_income_exclusion": {str(year): 1_000 for year in YEARS}, + } + }, + "spm_units": {"spm_unit": {"members": members}}, + "households": { + "household": { + "members": members, + "state_code": {str(year): "PR" for year in YEARS}, + } + }, + } + # Two independent simulations cover every year without repeated model + # construction or mutable caches shared between current law and AFA. + baseline = Simulation(situation=situation) + for year in YEARS: + assert baseline.calculate(BAR, year)[0] + for credit in CREDITS: + assert baseline.calculate(credit, year)[0] == 0 + + reform = Reform.from_dict( + {"gov.contrib.congress.afa.in_effect": {"2025-01-01.2100-12-31": True}}, + country_id="us", + ) + afa = Simulation(situation=situation, reform=reform) + # AFA applies to taxable years beginning after December 31, 2024. + assert afa.calculate(BAR, 2024)[0] + for year in (2025, 2039, 2040): + assert not afa.calculate(BAR, year)[0] + # Section 32(c)(1)(C) continues to deny EITC under AFA. + assert afa.calculate("eitc", year)[0] == 0 + expected = 825 if year == 2040 else afa.calculate("ctc", year)[0] + assert expected > 0 + for credit in CREDITS: + assert afa.calculate(credit, year)[0] == pytest.approx(expected)