Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
1f0cd1b
Make head_is_dependent_elsewhere consumers independent of the head label
MaxGhenis Oct 8, 2026
3ecc675
Keep tax units without a filer eligible where only claimed filers are…
MaxGhenis Oct 9, 2026
8da8312
Address review: Hawaii worksheet earnings, NM same-filer age, childle…
MaxGhenis Oct 9, 2026
e838623
Allow a cent of rounding in the Hawaii self-employed joint pair
MaxGhenis Oct 9, 2026
41fb4a5
Address review round 2: worksheet earnings, minors' own status, input…
MaxGhenis Oct 9, 2026
527d47f
Merge remote-tracking branch 'upstream/main' into head-dependent-else…
MaxGhenis Oct 9, 2026
436805e
Merge the Hawaii deductions references after syncing main
MaxGhenis Oct 9, 2026
23c6229
Apply the federal rules for a filer who can be claimed as a dependent
MaxGhenis Oct 9, 2026
e5e8327
Apply District of Columbia, Georgia and Idaho rules for a filer who c…
MaxGhenis Oct 10, 2026
255a571
Let either qualifying Georgia spouse claim the low income credit and …
MaxGhenis Oct 10, 2026
39cc805
Project the claimable-filer flag to members before combining with ages
MaxGhenis Oct 10, 2026
ad6f1e9
Address review: carry the claimable-filer rules into the GA, DC prope…
MaxGhenis Oct 10, 2026
4597c55
Address the federal claimable-filer review
MaxGhenis Oct 10, 2026
e4b8f36
Separate dependency facts and fix Hawaii qualified-person counts
MaxGhenis Oct 10, 2026
c7f969f
Clarify claimability outside the current tax unit
MaxGhenis Oct 10, 2026
4c8ed81
Merge commit 'c7f969f872' into claimable-filer-federal
MaxGhenis Oct 10, 2026
3f88702
Read the split dependency inputs from #10024
MaxGhenis Oct 10, 2026
b2771d2
Follow the corrected exemption count in three Hawaii credits
MaxGhenis Oct 10, 2026
163fcd4
Address the second federal claimable-filer review
MaxGhenis Oct 10, 2026
3d0864f
Address the third federal claimable-filer review
MaxGhenis Oct 10, 2026
35a2cbc
Give the spouse without an SSN a card type in the recovery rebate test
MaxGhenis Oct 10, 2026
d96f22b
Merge remote-tracking branch 'upstream/claimable-filer-federal' into …
MaxGhenis Oct 11, 2026
81ab8e4
Read the split claimable / claimed inputs from the settled federal br…
MaxGhenis Oct 11, 2026
0d2cdb8
Merge main into claimable-filer-dc-ga-id after #10087
MaxGhenis Oct 19, 2026
ae7ab34
Merge remote-tracking branch 'gh/main' into claimable-filer-dc-ga-id
MaxGhenis Oct 19, 2026
4633898
Exclude nonqualifying childcare from Idaho care deduction
MaxGhenis Oct 19, 2026
6421669
Clarify Georgia child credit restriction and mirror rebate fixture
MaxGhenis Oct 19, 2026
634bdc6
Cover filing exceptions in DC Georgia Idaho invariants
MaxGhenis Oct 19, 2026
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/claimable-filer-dc-ga-id.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Apply District of Columbia, Georgia and Idaho rules for a filer who can be claimed as a dependent: DC's property tax credit claimant, childless EITC, child care and child tax credit children; Georgia's dependent exemption, low income credit and 2023 surplus refund; Idaho's food tax credit, household and dependent care deduction and 2022 rebate. Exclude nonqualifying child-only daycare from Idaho's care deduction even when a disabled adult qualifies for care.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
description: The District of Columbia allows a person who was a dependent under an income tax law to claim the property tax credit only at or above this age.
values:
2021-01-01: 65
metadata:
unit: year
label: DC property tax credit minimum age for a dependent claimant
period: year
reference:
- title: Code of the District of Columbia § 47–1806.06 (k)
href: https://code.dccouncil.gov/us/dc/council/code/sections/47-1806.06
10 changes: 9 additions & 1 deletion policyengine_us/reforms/dc_kccatc.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,15 @@ def formula(tax_unit, period, parameters):
income_eligible = taxinc <= p.kccatc.income_limit[filing_status]
# determine count of KCCATC age eligible children
person = tax_unit.members
is_dependent = person("is_tax_unit_dependent", period)
# As in the baseline formula: no dependents on a return whose
# filer (or, if joint, either spouse) can be claimed.
filer_is_dependent = person.tax_unit(
"head_or_spouse_is_dependent_elsewhere_without_filing_exception",
period,
)
is_dependent = (
person("is_tax_unit_dependent", period) & ~filer_is_dependent
)
age = person("age", period)
kccatc_age_eligible = is_dependent & (age <= p.kccatc.max_age)
kccatc_eligible_count = tax_unit.sum(kccatc_age_eligible)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,15 @@ def formula(tax_unit, period, parameters):
# Income based eligibility criteria does not apply when the credit
# is phased out.
eligible = tax_unit("dc_ptc_eligible", period)
# The reform keeps D.C. Code 47-1806.06(k): no credit through a
# claimant who was a dependent, unless 65 or older.
has_claimant = tax_unit("dc_ptc_has_eligible_claimant", period)
if p.phase_out.applies:
income_limit = tax_unit("dc_ptc_income_limit", period)
income = tax_unit("adjusted_gross_income", period)
income_excess = max_(0, income - income_limit)
return max_(0, amount - p.phase_out.rate * income_excess)
return amount * eligible
return max_(0, amount - p.phase_out.rate * income_excess) * has_claimant
return amount * eligible * has_claimant

class dc_ptc_eligible(Variable):
value_type = bool
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@ def formula(tax_unit, period, parameters):

person = tax_unit.members
age = person("age", period)
is_dependent = person("is_tax_unit_dependent", period)
# A return on which the filer (or, if joint, either spouse) can be
# claimed as a dependent has no dependents (IRC 152(b)(1)), as in
# the baseline formula.
filer_is_dependent = person.tax_unit(
"head_or_spouse_is_dependent_elsewhere_without_filing_exception", period
)
is_dependent = person("is_tax_unit_dependent", period) & ~filer_is_dependent

# Apply age limit if in effect.
if p.age_limit.in_effect:
Expand Down
195 changes: 195 additions & 0 deletions policyengine_us/tests/core/test_claimable_filer_dc_ga_id_invariants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
"""District of Columbia, Georgia and Idaho rules for a filer who can be claimed
as a dependent.

A return on which the filer, or on a joint return either spouse, can be claimed
as a dependent outside the filing exception has no dependents (IRC 152(b)(1)).
DC's property tax credit needs a claimant who was not a dependent or is 65 or
older; Georgia's low income credit and 2023 surplus refund need one filer who
qualifies; Idaho's food tax credit leaves out each filer who can be claimed.

For couples drawn by Hypothesis and a seeded population in these states,
claimability, actual claims and the filing exception vary independently,
subject to an actual claim requiring claimability and excluding the exception:

1. Swap invariance: each output is the same under either head/spouse
labelling.
2. Monotonicity: marking another filer as claimed, with fallback claimability
and no filing exception, never raises the outputs in MONOTONE (amounts not
capped by a tax liability that the claim can raise).
3. Identities: a return with a claimable filer outside the filing exception has
no DC keep child care affordable credit or child tax credit and its Idaho
food tax credit is at most one filer's amount; a return on which every
filer can be claimed has no Georgia low income credit.

Hypothesis batches check 1 and 3; the seeded population checks 1, 2 and 3.
"""

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

from policyengine_us.tests.core.test_claimable_filer_federal_invariants import (
check_monotone,
check_swap,
)
from policyengine_us.tests.core.test_dependent_elsewhere_head_spouse_swap_invariance import (
CLAIM_PATTERNS,
_calc,
_crafted_cases,
_seeded_couples,
adults,
dependents,
)

STATES = ["DC", "GA", "ID"]
YEARS = [2021, 2023, 2025, 2026]
MONOTONE = [
"dc_ptc",
"dc_eitc",
"dc_kccatc",
"dc_ctc",
"ga_exemptions",
"ga_low_income_credit_potential",
"ga_ctc_potential",
"id_grocery_credit",
"id_household_and_dependent_care_expense_deduction",
]
OUTPUTS = MONOTONE + ["ga_surplus_tax_rebate", "id_2022_rebate"]


@st.composite
def couples(draw):
claimable = draw(st.sampled_from(CLAIM_PATTERNS))
claimed = tuple(can_claim and draw(st.booleans()) for can_claim in claimable)
return {
"state": draw(st.sampled_from(STATES)),
"adults": [draw(adults()), draw(adults())],
"dependents": draw(st.lists(dependents(), max_size=2)),
"claimed": claimed,
"claimable": claimable,
"exception": tuple(
not is_claimed and draw(st.booleans()) for is_claimed in claimed
),
}


def _identities(sim, year):
blocked_filer = (
_calc(
sim, "head_or_spouse_is_dependent_elsewhere_without_filing_exception", year
)
> 0
)
for name in ("dc_kccatc", "dc_ctc"):
assert not _calc(sim, name, year)[blocked_filer].any(), (
f"{name} in {year} is positive on a return with a claimable filer "
"outside the filing exception"
)
# Georgia's low income credit needs one filer who cannot be claimed.
every_filer = _calc(sim, "every_filer_is_dependent_elsewhere", year) > 0
assert not _calc(sim, "ga_low_income_credit_potential", year)[every_filer].any()
# Outside the filing exception, at most the independent filer keeps a
# food tax credit; no dependent child qualifies.
one_filer = _one_filer_food_amount(sim, year)
grocery = _calc(sim, "id_grocery_credit", year)
assert (grocery[blocked_filer] <= one_filer + 0.01).all()


def _one_filer_food_amount(sim, year):
p = sim.tax_benefit_system.parameters(f"{year}-01-01").gov.states.id.tax.income
amount = p.credits.grocery.base.amount
if p.credits.grocery.aged.in_effect:
amount += p.credits.grocery.aged.amount
return amount


def _with_independent_claim_inputs(units):
rng = np.random.default_rng(20261018)
result = []
for unit in units:
claimable = CLAIM_PATTERNS[int(rng.integers(len(CLAIM_PATTERNS)))]
claimed = tuple(can_claim and bool(rng.integers(2)) for can_claim in claimable)
exception = tuple(
not is_claimed and bool(rng.integers(2)) for is_claimed in claimed
)
result.append(
{
**unit,
"claimable": claimable,
"claimed": claimed,
"exception": exception,
}
)
return result


@pytest.mark.parametrize(
"year,state,expected",
[
(2025, "ID", {"id_grocery_credit": 310}),
(2026, "DC", {"dc_kccatc": 1_000, "dc_ctc": 1_000}),
],
)
def test_dc_id_filing_exception_restores_children_under_both_labels(
year, state, expected
):
# The claimable adult keeps the exception and the independent spouse and
# child keep their credits when either adult is labelled head. Idaho still
# excludes the claimable adult's own $155 food tax credit.
units = [
{
"state": state,
"adults": [{"age": 26, "employment_income": 2_000}, {"age": 30}],
"dependents": [{"age": 2, "pre_subsidy_childcare_expenses": 1_000}],
"claimed": (False, False),
"claimable": (True, False),
"exception": (True, False),
}
]
if state == "ID":
# Isolate the dependency rule from SNAP and other month exclusions.
for person in units[0]["adults"] + units[0]["dependents"]:
person["id_grocery_credit_qualified_months"] = 12

def exception_identities(sim, year):
for name, amount in expected.items():
np.testing.assert_allclose(_calc(sim, name, year), [amount, amount])
_identities(sim, year)

check_swap(units, year, OUTPUTS, exception_identities)


@settings(
max_examples=2,
deadline=None,
derandomize=True,
suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large],
)
@given(st.sampled_from(YEARS), st.lists(couples(), min_size=6, max_size=12))
def test_dc_ga_id_claimable_filer_rules_are_label_free(year, units):
check_swap(units, year, OUTPUTS, _identities)


@pytest.mark.parametrize("year", YEARS)
def test_dc_ga_id_claimable_filer_rules_seeded_population(year):
seeded = _seeded_couples(STATES, per_state=3)
crafted = _crafted_cases(STATES)
# Core fills omitted people with input defaults once any person supplies
# claimability, so express the crafted cases' legacy values explicitly in
# the swap simulation. The monotonicity simulation retains their fallback.
explicit_crafted = [
{**unit, "claimable": unit["claimed"], "exception": (False, False)}
for unit in crafted
]
check_swap(
_with_independent_claim_inputs(seeded) + explicit_crafted,
year,
OUTPUTS,
_identities,
)
# The shared monotonicity helper toggles actual claims while keeping any
# explicit claimability and exception inputs fixed. Retain the legacy
# population here so every variant is valid and claimability changes with
# the actual claim; the swap population independently exercises all inputs.
check_monotone(seeded + crafted, year, MONOTONE, _identities)
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,140 @@
state_code: DC
output:
dc_ctc_eligible_child: false


- name: 2026 single filer who can be claimed as a dependent, no DC child tax credit child
period: 2026
input:
people:
filer:
age: 30
employment_income: 10_000
is_tax_unit_head: true
is_tax_unit_spouse: false
claimed_as_dependent_on_another_return: true
child:
age: 3
tax_units:
tax_unit:
members: [filer, child]
households:
household:
members: [filer, child]
state_code: DC
output:
# 47-1806.17(a): a child for whom the taxpayer is allowed a
# deduction under IRC 151; IRC 152(b)(1).
dc_ctc_eligible_child: [false, false]

- name: 2026 joint return with one spouse who can be claimed as a dependent, no DC child tax credit child (the claimable spouse is the head)
period: 2026
input:
people:
person_a:
age: 30
employment_income: 5_000
is_tax_unit_head: true
is_tax_unit_spouse: false
claimed_as_dependent_on_another_return: true
person_b:
age: 30
employment_income: 5_000
is_tax_unit_head: false
is_tax_unit_spouse: true
child:
age: 3
tax_units:
tax_unit:
members: [person_a, person_b, child]
filing_status: JOINT
households:
household:
members: [person_a, person_b, child]
state_code: DC
output:
dc_ctc_eligible_child: [false, false, false]

- name: 2026 joint return with one spouse who can be claimed as a dependent, no DC child tax credit child (the claimable spouse is the spouse)
period: 2026
input:
people:
person_b:
age: 30
employment_income: 5_000
is_tax_unit_head: true
is_tax_unit_spouse: false
person_a:
age: 30
employment_income: 5_000
is_tax_unit_head: false
is_tax_unit_spouse: true
claimed_as_dependent_on_another_return: true
child:
age: 3
tax_units:
tax_unit:
members: [person_b, person_a, child]
filing_status: JOINT
households:
household:
members: [person_b, person_a, child]
state_code: DC
output:
dc_ctc_eligible_child: [false, false, false]

- name: 2026 joint return with both spouses claimable as dependents, no DC child tax credit child
period: 2026
input:
people:
person_a:
age: 30
employment_income: 5_000
is_tax_unit_head: true
is_tax_unit_spouse: false
claimed_as_dependent_on_another_return: true
person_b:
age: 30
employment_income: 5_000
is_tax_unit_head: false
is_tax_unit_spouse: true
claimed_as_dependent_on_another_return: true
child:
age: 3
tax_units:
tax_unit:
members: [person_a, person_b, child]
filing_status: JOINT
households:
household:
members: [person_a, person_b, child]
state_code: DC
output:
dc_ctc_eligible_child: [false, false, false]

- name: 2026 joint return with neither spouse claimable, no DC child tax credit child
period: 2026
input:
people:
person_a:
age: 30
employment_income: 5_000
is_tax_unit_head: true
is_tax_unit_spouse: false
person_b:
age: 30
employment_income: 5_000
is_tax_unit_head: false
is_tax_unit_spouse: true
child:
age: 3
tax_units:
tax_unit:
members: [person_a, person_b, child]
filing_status: JOINT
households:
household:
members: [person_a, person_b, child]
state_code: DC
output:
dc_ctc_eligible_child: [false, false, true]
Loading