diff --git a/changelog.d/hub-crfb-surtax-pretax-premiums-1010.fixed.md b/changelog.d/hub-crfb-surtax-pretax-premiums-1010.fixed.md new file mode 100644 index 00000000000..95eeb37260e --- /dev/null +++ b/changelog.d/hub-crfb-surtax-pretax-premiums-1010.fixed.md @@ -0,0 +1 @@ +Correct CRFB expanded surtax premium addbacks to use employer-paid coverage and employee pretax premiums without double-counting after-tax employee premiums. diff --git a/policyengine_us/parameters/gov/contrib/crfb/surtax/increased_base/sources.yaml b/policyengine_us/parameters/gov/contrib/crfb/surtax/increased_base/sources.yaml index a978ed74f69..0aebd7562d2 100644 --- a/policyengine_us/parameters/gov/contrib/crfb/surtax/increased_base/sources.yaml +++ b/policyengine_us/parameters/gov/contrib/crfb/surtax/increased_base/sources.yaml @@ -27,8 +27,13 @@ values: - foreign_earned_income_exclusion # Tax-free bond interest - tax_exempt_interest_income - # Employer-provided health insurance (premiums) - - health_insurance_premiums + # Employer-provided health insurance: employer-paid premiums and the + # employee's pretax payroll contribution. Including the employee's + # pretax contribution is inferred from the broad income definition and + # IRS Rev. Rul. 2002-3's treatment of salary-reduction premiums as + # employer-provided coverage; after-tax premiums use income in AGI. + - employer_sponsored_insurance_premiums + - pre_tax_health_insurance_premiums metadata: unit: list @@ -37,3 +42,5 @@ metadata: reference: - title: "Committee for a Responsible Federal Budget, Break Glass: A Plan for the Next Economic Shock, footnote 2" href: https://www.crfb.org/papers/break-glass-plan-next-economic-shock + - title: "IRS Revenue Ruling 2002-3, salary-reduction health insurance premiums #page 1-2" + href: https://www.irs.gov/pub/irs-drop/rr-02-3.pdf#page=1 diff --git a/policyengine_us/tests/core/test_dependent_payroll_taxes_ctc_and_surtax_invariants.py b/policyengine_us/tests/core/test_dependent_payroll_taxes_ctc_and_surtax_invariants.py index 3a71438bb9e..9082faacae4 100644 --- a/policyengine_us/tests/core/test_dependent_payroll_taxes_ctc_and_surtax_invariants.py +++ b/policyengine_us/tests/core/test_dependent_payroll_taxes_ctc_and_surtax_invariants.py @@ -11,9 +11,9 @@ expanded base adds amounts to this return's AGI, so it counts the head's and spouse's own contributions, exclusions and deductions too. -Each case runs every tax unit twice in one vectorized simulation: once with -the dependents' amounts as drawn and once with them set to zero. For every -tax unit: +Dependent-exclusion cases run every tax unit twice in one vectorized simulation: +once with the dependents' amounts as drawn and once with them set to zero. For +every tax unit: 1. A dependent's wages, self-employment income, retirement contributions, tax-exempt interest, Social Security benefits and health insurance @@ -29,6 +29,9 @@ 3. Differential: `ctc_social_security_tax`, the Puerto Rico equivalent, `additional_medicare_tax` and the surtax equal an independent numpy calculation over the head and spouse. +4. With AGI held fixed, employer-paid and qualified salary-reduction premiums + add the same amount to the surtax base; after-tax premiums add nothing. + Generated payment-method cases cover both surtax thresholds. Invariant 2 leaves out three kinds of dependent amount, which invariant 1 checks with AGI and its taxable Social Security given as inputs: @@ -67,12 +70,15 @@ "traditional_403b_contributions_desired", "social_security_dependents", ] -# Dependent amounts that reach the filer's return elsewhere: see the module -# docstring. +# Additional dependent amounts checked with AGI held fixed, including all +# three premium payment methods. Some reach the return elsewhere: see the +# module docstring. OTHER_INPUTS = [ "self_employment_income", "tax_exempt_interest_income", "health_insurance_premiums", + "employer_sponsored_insurance_premiums", + "pre_tax_health_insurance_premiums", ] PERSON_PAYROLL_TAXES = [ "employee_social_security_tax", @@ -85,7 +91,8 @@ "traditional_403b_contributions", "student_loan_interest_ald", "tax_exempt_interest_income", - "health_insurance_premiums", + "employer_sponsored_insurance_premiums", + "pre_tax_health_insurance_premiums", ] money = st.integers(0, 60_000).map(float) @@ -116,6 +123,12 @@ def filer(draw): "traditional_401k_contributions_desired": draw(maybe(money)), "tax_exempt_interest_income": draw(maybe(money)), "health_insurance_premiums": draw(maybe(st.integers(1, 9_000).map(float))), + "employer_sponsored_insurance_premiums": draw( + maybe(st.integers(1, 9_000).map(float)) + ), + "pre_tax_health_insurance_premiums": draw( + maybe(st.integers(1, 9_000).map(float)) + ), } @@ -140,6 +153,12 @@ def dependent(draw, *, every_amount): person["health_insurance_premiums"] = draw( maybe(st.integers(1, 9_000).map(float)) ) + person["employer_sponsored_insurance_premiums"] = draw( + maybe(st.integers(1, 9_000).map(float)) + ) + person["pre_tax_health_insurance_premiums"] = draw( + maybe(st.integers(1, 9_000).map(float)) + ) return person @@ -394,6 +413,82 @@ def test_dependents_amounts_never_change_payroll_taxes_or_surtax_additions(units check_every_amount_with_agi_given(units, year) +def test_after_tax_premiums_do_not_increase_surtax_with_agi_given(): + year = 2026 + unit = { + "head": {"age": 40, "health_insurance_premiums": 4_000.0}, + "spouse": None, + "dependents": [{"age": 17, "employment_income": 0.0}], + "excess_payroll_tax_withheld": 0.0, + "adjusted_gross_income": 150_000.0, + } + situation, filers, unit_index = build([unit], year) + situation["tax_units"]["tax_unit_0"]["filing_status"] = {year: "SINGLE"} + simulation = Simulation(situation=situation, tax_benefit_system=surtax_system()) + # After-tax premiums are already financed from the supplied AGI. + # The stale oracle added $4,000 again, incorrectly requiring $540. + np.testing.assert_allclose( + calculate(simulation, "agi_surtax", year), [500.0], atol=TOLERANCE + ) + assert_surtax_matches_reference(simulation, filers, unit_index, year) + + +@SETTINGS +@given( + amount=st.integers(10, 60_000).map(float), + dependent_premiums=st.tuples( + st.integers(10, 60_000).map(float), + st.integers(10, 60_000).map(float), + st.integers(10, 60_000).map(float), + ), + married=st.booleans(), + year=st.sampled_from(YEARS), +) +def test_surtax_premium_payment_methods_with_agi_given( + amount, dependent_premiums, married, year +): + system = surtax_system() + p = system.parameters(year).gov.contrib.crfb.surtax + scale = p.rate.joint if married else p.rate.single + # Every draw covers a below-threshold base, crossing the first threshold, + # the 1% bracket and crossing the top threshold. + agi = np.array( + [ + 0.0, + scale.thresholds[1] - amount / 2, + scale.thresholds[1] + amount, + scale.thresholds[2] - amount / 2, + ] + ) + methods = ( + "employer_sponsored_insurance_premiums", + "pre_tax_health_insurance_premiums", + "health_insurance_premiums", + ) + units = [ + { + "head": {"age": 40, method: amount}, + "spouse": {"age": 40} if married else None, + "dependents": [{"age": 17, **dict(zip(methods, dependent_premiums))}], + "excess_payroll_tax_withheld": 0.0, + "adjusted_gross_income": float(income), + } + for method in methods + for income in agi + ] + situation, filers, unit_index = build(units, year) + simulation = Simulation(situation=situation, tax_benefit_system=system) + employer_paid, salary_reduction, after_tax = np.split( + calculate(simulation, "agi_surtax", year), 3 + ) + np.testing.assert_allclose(employer_paid, salary_reduction, atol=TOLERANCE) + # With AGI fixed, switching to after-tax payments lowers the expanded + # base by exactly the moved amount. Apply the scale across both brackets. + np.testing.assert_allclose(employer_paid, scale.calc(agi + amount), atol=TOLERANCE) + np.testing.assert_allclose(after_tax, scale.calc(agi), atol=TOLERANCE) + assert_surtax_matches_reference(simulation, filers, unit_index, year) + + def low_wage_family(married, dependent_wages): """Three qualifying children, the EITC barred by interest income and wages low enough that payroll taxes decide the refundable CTC.""" diff --git a/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml b/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml index f505b72c1c1..555138b30c2 100644 --- a/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml +++ b/policyengine_us/tests/policy/contrib/crfb/agi_surtax.yaml @@ -18,6 +18,62 @@ output: agi_surtax: 500 +- name: Expanded base restores employee pretax premiums and adds employer-paid premiums. + period: 2026 + reforms: policyengine_us.reforms.crfb.agi_surtax.agi_surtax_reform_object + input: + gov.contrib.crfb.surtax.in_effect: true + gov.contrib.crfb.surtax.increased_base.in_effect: true + employment_income: 150_000 + health_insurance_premiums: 4_000 + pre_tax_health_insurance_premiums: 6_000 + employer_sponsored_insurance_premiums: 10_000 + filing_status: SINGLE + output: + adjusted_gross_income: 144_000 + # 144,000 AGI + 6,000 pretax + 10,000 employer premiums = 160,000. + agi_surtax: 600 + +- name: Expanded base is unchanged when employee premiums are paid entirely after tax. + period: 2026 + reforms: policyengine_us.reforms.crfb.agi_surtax.agi_surtax_reform_object + input: + gov.contrib.crfb.surtax.in_effect: true + gov.contrib.crfb.surtax.increased_base.in_effect: true + employment_income: 150_000 + health_insurance_premiums: 10_000 + employer_sponsored_insurance_premiums: 10_000 + filing_status: SINGLE + output: + adjusted_gross_income: 150_000 + # 150,000 AGI + 10,000 employer premiums = the same 160,000 base. + agi_surtax: 600 + +- name: Expanded base does not add after-tax employee premiums to supplied AGI. + period: 2026 + reforms: policyengine_us.reforms.crfb.agi_surtax.agi_surtax_reform_object + input: + gov.contrib.crfb.surtax.in_effect: true + gov.contrib.crfb.surtax.increased_base.in_effect: true + adjusted_gross_income: 110_000 + health_insurance_premiums: 20_000 + filing_status: SINGLE + output: + agi_surtax: 100 + +- name: Expanded base restores pretax employee premiums excluded from supplied AGI. + period: 2026 + reforms: policyengine_us.reforms.crfb.agi_surtax.agi_surtax_reform_object + input: + gov.contrib.crfb.surtax.in_effect: true + gov.contrib.crfb.surtax.increased_base.in_effect: true + adjusted_gross_income: 110_000 + pre_tax_health_insurance_premiums: 20_000 + filing_status: SINGLE + output: + agi_surtax: 300 + income_tax_before_credits: 15_670 + - name: Joint couple below threshold period: 2026 reforms: policyengine_us.reforms.crfb.agi_surtax.agi_surtax_reform_object