Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/aca-ptc-files-on-take-up.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Count a tax unit that takes up the ACA premium tax credit as filing a return to claim it (26 CFR 1.6011-8(a)), so the filing gate on aca_ptc and on the California, Maryland and New Mexico premium programs no longer zeroes the credit of a tax unit that takes it up but would not otherwise file; a tax unit that neither files nor takes up the credit still gets none.
127 changes: 127 additions & 0 deletions policyengine_us/tests/core/test_aca_ptc_filing_gate_invariants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""Invariants for the filing gate on the ACA premium tax credit.

`aca_ptc` is the credit for a tax unit that files a return on which it can
claim it (`files_return_for_aca_ptc`): one that files for any reason
(`tax_unit_is_filer`) or that takes up the credit, since a taxpayer who
receives advance payments must file and reconcile them (26 CFR 1.6011-8(a)).
Hypothesis draws tax units with any benchmark premium, household income and
required contribution, and runs each in all four combinations of filing and
take-up. Each drawn example is one simulation, calculated for both years.
For every tax unit:

1. Taking up the credit never lowers `aca_ptc`.
2. For a tax unit that takes up the credit, `aca_ptc` and `assigned_aca_ptc`
are the same whether or not it would file for another reason.
3. A tax unit that neither files nor takes up the credit gets no `aca_ptc`
(the filing gate added for IRS filer data).
4. A tax unit that files gets the same `aca_ptc` whether or not it takes up
the credit.
5. Taking up coverage when the credit is $0 is no reason to file: a non-filer
that takes up but whose credit before the filing gate is $0 does not file
for it (`files_return_for_aca_ptc` is false), so state programs that read
it stay closed.

Why Python: the YAML cases pin one combination each with fixed amounts; this
checks every combination of filing and take-up against drawn benchmark
premiums, incomes and contribution rates in one vectorized simulation, so a
gate that drops the take-up leg, or that makes the credit depend on filing for
a unit that takes it up, fails whatever the amounts. Cost, measured locally
(macOS, shared host): about 4.5 s of test calls, under 10 s in pytest and 26
to 39 s wall time including the model import, peak memory footprint 1.6 GB. It runs in the
`core` group of the rest Python tests.
"""

import numpy as np
from hypothesis import HealthCheck, given, settings
from hypothesis import strategies as st

from policyengine_us import Simulation

YEARS = (2025, 2026)
# (tax_unit_is_filer, takes_up_aca_if_eligible) for the four copies of a unit.
COMBINATIONS = [(False, False), (False, True), (True, False), (True, True)]

units = st.tuples(
st.integers(0, 30_000), # slcsp
st.integers(0, 120_000), # aca_magi
st.sampled_from([0.0, 0.02, 0.04, 0.085, 0.0996]),
)


def simulate(rows):
people, tax_units, households = {}, {}, {}
groups = {"marital_units": {}, "spm_units": {}, "families": {}}
index = 0
for slcsp, magi, share in rows:
for files, takes_up in COMBINATIONS:
person, members = f"person_{index}", [f"person_{index}"]
people[person] = {
"age": {year: 40 for year in YEARS},
"is_aca_ptc_eligible": {year: True for year in YEARS},
}
tax_units[f"tax_unit_{index}"] = {
"members": members,
"slcsp": {year: slcsp for year in YEARS},
"aca_magi": {year: magi for year in YEARS},
"aca_required_contribution_percentage": {year: share for year in YEARS},
"tax_unit_is_filer": {year: files for year in YEARS},
"takes_up_aca_if_eligible": {year: takes_up for year in YEARS},
}
households[f"household_{index}"] = {
"members": members,
"state_code": {year: "TX" for year in YEARS},
}
for name in groups:
groups[name][f"{name}_{index}"] = {"members": members}
index += 1
return Simulation(
situation={
"people": people,
"tax_units": tax_units,
"households": households,
**groups,
}
)


@settings(
max_examples=6,
deadline=None,
derandomize=True,
suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large],
)
@given(
st.lists(units, min_size=4, max_size=24).map(
# A unit whose credit is positive, so properties 1-4 compare something, and
# one whose credit is zero, so property 5 does.
lambda rows: [(12_000, 30_000, 0.04), (5_500, 60_000, 0.0996), *rows]
)
)
def test_aca_ptc_filing_gate(rows):
simulation = simulate(rows)
for year in YEARS:
ptc = np.asarray(simulation.calculate("aca_ptc", year)).reshape(-1, 4)
assigned = np.asarray(simulation.calculate("assigned_aca_ptc", year)).reshape(
-1, 4
)
neither, takes_up_only, files_only, both = ptc.T
assert (ptc >= 0).all()
assert (both > 0).any(), "no unit with a positive credit"
# Property 1.
assert (takes_up_only >= neither).all() and (both >= files_only).all()
# Property 2.
np.testing.assert_allclose(takes_up_only, both)
np.testing.assert_allclose(assigned[:, 1], assigned[:, 3])
# Property 3.
assert (neither == 0).all()
# Property 4.
np.testing.assert_allclose(files_only, both)
# Property 5.
before = np.asarray(
simulation.calculate("aca_ptc_before_filing_gate", year)
).reshape(-1, 4)[:, 1]
files = np.asarray(
simulation.calculate("files_return_for_aca_ptc", year)
).reshape(-1, 4)[:, 1]
np.testing.assert_array_equal(files, before > 0)
assert (before == 0).any() and (before > 0).any()
22 changes: 21 additions & 1 deletion policyengine_us/tests/policy/baseline/gov/aca/ptc/aca_ptc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@
output:
aca_ptc: 200

- name: ACA PTC is zero for non-filers (IRC 36B(a) requires filing Form 8962)
- name: ACA PTC is zero for non-filers who do not take it up (IRC 36B(a) requires filing Form 8962)
absolute_error_margin: 0.01
period: 2025
input:
Expand All @@ -141,9 +141,29 @@
aca_magi: 25_000
aca_required_contribution_percentage: 0.04
tax_unit_is_filer: false
# Nor does it take up the federal credit, so it files no return to claim it.
takes_up_aca_if_eligible: false
output:
aca_ptc: 0

- name: A tax unit that takes up the PTC files to claim it, even if it would not file otherwise
# 26 CFR 1.6011-8(a): a taxpayer who receives advance payments "must file
# an income tax return for that taxable year ... and reconcile the advance
# credit payments"; a credit not paid in advance is claimed on Form 8962.
absolute_error_margin: 0.01
period: 2025
input:
is_aca_ptc_eligible: true
slcsp: 1_200
aca_magi: 25_000
aca_required_contribution_percentage: 0.04
tax_unit_is_filer: false
takes_up_aca_if_eligible: true
output:
files_return_for_aca_ptc: true
aca_ptc: 200 # 1200 - 0.04 * 25000 = 200
assigned_aca_ptc: 200

- name: ACA PTC is paid to eligible filers (regression of filer gate)
absolute_error_margin: 0.01
period: 2025
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# A tax unit files a return that can claim the premium tax credit if it files
# for any reason, or if it takes up the credit (26 CFR 1.6011-8(a); Form 1040
# instructions, Chart C, item 5).

- name: A filer that does not take up the credit
period: 2025
input:
tax_unit_is_filer: true
takes_up_aca_if_eligible: false
output:
files_return_for_aca_ptc: true

- name: A non-filer that takes up a positive credit
period: 2025
input:
is_aca_ptc_eligible: true
slcsp: 1_200
aca_magi: 25_000
aca_required_contribution_percentage: 0.04
tax_unit_is_filer: false
takes_up_aca_if_eligible: true
output:
aca_ptc_before_filing_gate: 200 # 1200 - 0.04 * 25000
files_return_for_aca_ptc: true

- name: A non-filer that takes up coverage when the credit is zero does not file for it
# $5,500 benchmark less 9.96% of $60,000 ($5,976) is below zero: there is
# no credit to claim and no advance payment to reconcile.
period: 2026
input:
is_aca_ptc_eligible: true
slcsp: 5_500
aca_magi: 60_000
aca_required_contribution_percentage: 0.0996
tax_unit_is_filer: false
takes_up_aca_if_eligible: true
output:
aca_ptc_before_filing_gate: 0
files_return_for_aca_ptc: false

- name: A non-filer that does not take up the credit
period: 2025
input:
tax_unit_is_filer: false
takes_up_aca_if_eligible: false
output:
files_return_for_aca_ptc: false

- name: Retired couple with only Social Security is not required to file but files to claim the credit
# Social Security of $30,000 and nothing else: none of it is taxable (the
# provisional income of $15,000 is under the $32,000 joint base amount), so
# gross income is zero and no return is required, and there is no earned
# income for an EITC. Taking up the credit makes it a filer for the credit.
period: 2025
input:
people:
head:
age: 62
social_security_retirement: 15_000
spouse:
age: 62
social_security_retirement: 15_000
tax_units:
tax_unit:
members: [head, spouse]
households:
household:
members: [head, spouse]
state_code: TX
output:
taxable_social_security: [0, 0]
tax_unit_is_required_to_file: false
tax_unit_is_filer: false
files_return_for_aca_ptc: true
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@
members: [person1]
aca_magi_fraction: 1.40
tax_unit_is_filer: false
# Nor does it take up the federal credit, so it files no return to claim it.
takes_up_aca_if_eligible: false
households:
household:
members: [person1]
Expand Down Expand Up @@ -401,3 +403,79 @@
state_code: CA
output:
ca_premium_subsidy_eligible: false

- name: Case 13b, an in-band CA unit that takes up the federal credit files and is eligible.
# Case 13 with take-up true and a positive federal credit ($6,000 benchmark
# less 4.19% of $22,000 = $5,078.20): a tax unit that takes up the credit
# files a return to claim or reconcile it (26 CFR 1.6011-8(a)), so the
# filing gate passes.
period: 2026
input:
people:
person1:
age: 40
is_aca_ptc_eligible: true
tax_units:
tax_unit:
members: [person1]
aca_magi_fraction: 1.40
slcsp: 6_000
aca_magi: 22_000
aca_required_contribution_percentage: 0.0419
tax_unit_is_filer: false
takes_up_aca_if_eligible: true
households:
household:
members: [person1]
state_code: CA
output:
ca_premium_subsidy_eligible: true

- name: Case 13c, an in-band CA non-filer that takes up coverage with a zero federal credit is ineligible.
# The federal contribution (4% of $22,000 = $880) exceeds the $500
# benchmark, so there is no federal credit and no reason to file. Filer twin:
# Case 13d.
period: 2026
input:
people:
person1:
age: 40
is_aca_ptc_eligible: true
tax_units:
tax_unit:
members: [person1]
aca_magi_fraction: 1.40
slcsp: 500
aca_magi: 22_000
aca_required_contribution_percentage: 0.04
tax_unit_is_filer: false
takes_up_aca_if_eligible: true
households:
household:
members: [person1]
state_code: CA
output:
ca_premium_subsidy_eligible: false

- name: Case 13d, the same unit filing for another reason is eligible.
period: 2026
input:
people:
person1:
age: 40
is_aca_ptc_eligible: true
tax_units:
tax_unit:
members: [person1]
aca_magi_fraction: 1.40
slcsp: 500
aca_magi: 22_000
aca_required_contribution_percentage: 0.04
tax_unit_is_filer: true
takes_up_aca_if_eligible: true
households:
household:
members: [person1]
state_code: CA
output:
ca_premium_subsidy_eligible: true
Loading
Loading