Skip to content
Draft
1 change: 1 addition & 0 deletions changelog.d/claimable-filer-va.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Apply Virginia's rules for a filer who can be claimed as a dependent: the standard deduction is limited to the filers' earned income (Va. Code 58.1-322.03(1)(b)), personal exemptions follow those allowable federally (also under the dependent exemption reform), and the EITC alternatives are barred (58.1-339.8(B)(1)); the aged and blind exemption now goes only to aged or blind filers, while a blind or aged dependent still bars the low-income credit (58.1-339.8(D)(4)).
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ def formula(tax_unit, period, parameters):

person = tax_unit.members
age = person("age", period)
is_dependent = person("is_tax_unit_dependent", period)
# As in the baseline, a return on which the filer (or, if joint,
# either spouse) can be claimed as a dependent has no dependents
# (IRC 152(b)(1)), unless the would-be claimant meets the filing
# exception.
is_dependent = person("is_tax_unit_dependent", period) & ~person.tax_unit(
"head_or_spouse_is_dependent_elsewhere_without_filing_exception", period
)

# Apply age limit if in effect
if p.age_limit.in_effect:
Expand Down Expand Up @@ -83,7 +89,13 @@ class va_older_dependents_count(Variable):

def formula(tax_unit, period, parameters):
person = tax_unit.members
is_dependent = person("is_tax_unit_dependent", period)
# As in the baseline, a return on which the filer (or, if joint,
# either spouse) can be claimed as a dependent has no dependents
# (IRC 152(b)(1)), unless the would-be claimant meets the filing
# exception.
is_dependent = person("is_tax_unit_dependent", period) & ~person.tax_unit(
"head_or_spouse_is_dependent_elsewhere_without_filing_exception", period
)
total_dependents = tax_unit.sum(is_dependent)
eligible_dependent_exemptions = tax_unit(
"va_eligible_dependents_count", period
Expand All @@ -104,14 +116,14 @@ def formula(tax_unit, period, parameters):
# Calculate personal exemptions base amount (filer + spouse + older dependents)
filing_status = tax_unit("filing_status", period)
older_dependents = tax_unit("va_older_dependents_count", period)
personal_count = (
where(
filing_status == filing_status.possible_values.JOINT,
2,
1,
)
+ older_dependents
# As in the baseline, a filer whom another taxpayer can claim has
# no exemption of their own (IRC 151(d)(2)).
filers = where(
tax_unit("head_or_spouse_is_dependent_elsewhere", period),
tax_unit("head_spouse_count_not_dependent_elsewhere", period),
where(filing_status == filing_status.possible_values.JOINT, 2, 1),
)
personal_count = filers + older_dependents
personal_exemption_amount = personal_count * p_base

# Add dependent exemption (has its own phase-out logic)
Expand Down
70 changes: 70 additions & 0 deletions policyengine_us/tests/core/test_claimable_filer_va_invariants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Virginia rules for a filer who can be claimed as a dependent.

The standard deduction is limited to the filers' earned income, personal
exemptions follow those allowable federally, and the EITC alternatives are
barred (Va. Code 58.1-322.03, 58.1-339.8).

For couples with either, both or neither spouse claimed:

1. Swap invariance: each tax-unit output is the same under either head/spouse
labelling (Hypothesis batches and the seeded population).
2. Monotonicity: marking another filer as claimed never raises an output
(seeded population only).

The outputs are tax-unit totals; which spouse an exemption goes to is checked
by YAML cases on the person-level variables.
"""

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,
_crafted_cases,
_seeded_couples,
adults,
dependents,
)

STATES = ["VA"]
YEARS = [2023, 2025, 2026]
OUTPUTS = [
"va_standard_deduction",
"va_personal_exemption",
"va_aged_blind_exemption",
"va_refundable_eitc_if_claimed",
"va_non_refundable_eitc_if_claimed",
]


@st.composite
def couples(draw):
return {
"state": draw(st.sampled_from(STATES)),
"adults": [draw(adults()), draw(adults())],
"dependents": draw(st.lists(dependents(), max_size=2)),
"claimed": draw(st.sampled_from(CLAIM_PATTERNS)),
}


@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_va_claimable_filer_rules_are_label_free(year, units):
check_swap(units, year, OUTPUTS)


@pytest.mark.parametrize("year", YEARS)
def test_va_claimable_filer_rules_seeded_population(year):
units = _seeded_couples(STATES, per_state=3) + _crafted_cases(STATES)
check_swap(units, year, OUTPUTS)
check_monotone(units, year, OUTPUTS)
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,77 @@
output:
# 2025: 20% of federal EITC
va_non_refundable_eitc_if_claimed: 800


- name: 2025 joint return with one claimable spouse gets no nonrefundable Virginia EITC (the claimable spouse is the head)
period: 2025
input:
people:
person_a:
age: 30
claimed_as_dependent_on_another_return: true
is_tax_unit_head: true
is_tax_unit_spouse: false
person_b:
age: 30
is_tax_unit_head: false
is_tax_unit_spouse: true
tax_units:
tax_unit:
members: [person_a, person_b]
filing_status: JOINT
eitc: 1000
va_low_income_tax_credit: 0
households:
household:
members: [person_a, person_b]
state_code: VA
output:
va_non_refundable_eitc_if_claimed: 0

- name: 2025 joint return with one claimable spouse gets no nonrefundable Virginia EITC (the claimable spouse is the spouse)
period: 2025
input:
people:
person_b:
age: 30
is_tax_unit_head: true
is_tax_unit_spouse: false
person_a:
age: 30
claimed_as_dependent_on_another_return: true
is_tax_unit_head: false
is_tax_unit_spouse: true
tax_units:
tax_unit:
members: [person_b, person_a]
filing_status: JOINT
eitc: 1000
va_low_income_tax_credit: 0
households:
household:
members: [person_b, person_a]
state_code: VA
output:
va_non_refundable_eitc_if_claimed: 0

- name: 2025 joint return with neither spouse claimable gets the nonrefundable Virginia EITC
period: 2025
input:
people:
person_a:
age: 30
person_b:
age: 30
tax_units:
tax_unit:
members: [person_a, person_b]
filing_status: JOINT
eitc: 1000
va_low_income_tax_credit: 0
households:
household:
members: [person_a, person_b]
state_code: VA
output:
va_non_refundable_eitc_if_claimed: 200
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,76 @@
eitc: 5_000
output:
va_refundable_eitc_if_claimed: 0


- name: 2025 joint return with one claimable spouse gets no Virginia EITC (the claimable spouse is the head)
period: 2025
input:
people:
person_a:
age: 30
claimed_as_dependent_on_another_return: true
is_tax_unit_head: true
is_tax_unit_spouse: false
person_b:
age: 30
is_tax_unit_head: false
is_tax_unit_spouse: true
tax_units:
tax_unit:
members: [person_a, person_b]
filing_status: JOINT
eitc: 1000
households:
household:
members: [person_a, person_b]
state_code: VA
output:
# Va. Code 58.1-339.8(B)(1).
va_refundable_eitc_if_claimed: 0

- name: 2025 joint return with one claimable spouse gets no Virginia EITC (the claimable spouse is the spouse)
period: 2025
input:
people:
person_b:
age: 30
is_tax_unit_head: true
is_tax_unit_spouse: false
person_a:
age: 30
claimed_as_dependent_on_another_return: true
is_tax_unit_head: false
is_tax_unit_spouse: true
tax_units:
tax_unit:
members: [person_b, person_a]
filing_status: JOINT
eitc: 1000
households:
household:
members: [person_b, person_a]
state_code: VA
output:
va_refundable_eitc_if_claimed: 0

- name: 2025 joint return with neither spouse claimable gets the Virginia EITC
period: 2025
input:
people:
person_a:
age: 30
person_b:
age: 30
tax_units:
tax_unit:
members: [person_a, person_b]
filing_status: JOINT
eitc: 1000
households:
household:
members: [person_a, person_b]
state_code: VA
output:
# 20% x $1,000.
va_refundable_eitc_if_claimed: 200
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,80 @@
state_code: VA
output:
va_low_income_tax_credit_eligible: false

# Va. Code 58.1-339.8(D)(4): no credit when "any person claimed as a dependent"
# claims the additional exemption for blind or aged taxpayers on their own
# return. The model takes a blind or aged dependent to claim it.
- name: 2025 blind dependent bars the low income tax credit
period: 2025
input:
people:
parent:
age: 40
is_tax_unit_head: true
is_tax_unit_spouse: false
child:
age: 16
is_blind: true
is_tax_unit_head: false
is_tax_unit_spouse: false
tax_units:
tax_unit:
members: [parent, child]
households:
household:
members: [parent, child]
state_code: VA
output:
va_aged_blind_exemption: 0
va_low_income_tax_credit_eligible: false

- name: 2025 aged dependent bars the low income tax credit
period: 2025
input:
people:
person_a:
age: 40
is_tax_unit_head: true
is_tax_unit_spouse: false
person_b:
age: 40
is_tax_unit_head: false
is_tax_unit_spouse: true
parent:
age: 70
is_tax_unit_head: false
is_tax_unit_spouse: false
tax_units:
tax_unit:
members: [person_a, person_b, parent]
filing_status: JOINT
households:
household:
members: [person_a, person_b, parent]
state_code: VA
output:
va_aged_blind_exemption: 0
va_low_income_tax_credit_eligible: false

- name: 2025 dependent who is neither blind nor aged does not bar the low income tax credit
period: 2025
input:
people:
parent:
age: 40
is_tax_unit_head: true
is_tax_unit_spouse: false
child:
age: 16
is_tax_unit_head: false
is_tax_unit_spouse: false
tax_units:
tax_unit:
members: [parent, child]
households:
household:
members: [parent, child]
state_code: VA
output:
va_low_income_tax_credit_eligible: true
Loading