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/fix-va-529-owner-taxable-income.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Virginia's Commonwealth Savers (529) deduction now goes to the account owner of record, including the full deduction for owners age 70 or older, and is taken from Virginia adjusted gross income on Form 760 line 13 rather than subtracted in computing it.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
description: Virginia lets an account owner who has attained this age deduct the full amount contributed to Commonwealth Savers accounts during the year, without the per-account cap.
values:
2021-01-01: 70
metadata:
unit: year
period: year
label: Virginia 529 plan contribution deduction full-deduction age
reference:
- title: Va. Code § 58.1-322.03(7)(b)
href: https://law.lis.virginia.gov/vacode/title58.1/chapter3/section58.1-322.03/
- title: 2024 Form 760 Instructions, Deductions from Income, Code 104
href: https://www.tax.virginia.gov/sites/default/files/vatax-pdf/2024-760-instructions.pdf#page=28
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
description: >
Virginia caps the 529 plan contributions deduction at this amount per
account per year. Taxpayers age 70 and over may deduct the entire
amount contributed.
# Retain the legacy subtractions parameter path for reform compatibility.
description: Virginia caps the Commonwealth Savers account contributions deduction at this amount per account per year.
values:
2021-01-01: 4_000
metadata:
unit: currency-USD
period: year
label: Virginia 529 plan contribution deduction cap per account
reference:
- title: Virginia Tax - Deductions
href: https://www.tax.virginia.gov/deductions
- title: Code of Virginia 58.1-322.02
href: https://law.lis.virginia.gov/vacodefull/title58.1/chapter3/article2/
- title: 2024 Form 760 Instructions - Subtractions, Certification Number 34
href: https://www.tax.virginia.gov/sites/default/files/vatax-pdf/2024-760-instructions.pdf#page=24
- title: Va. Code § 58.1-322.03(7)(a)
href: https://law.lis.virginia.gov/vacode/title58.1/chapter3/section58.1-322.03/
- title: 2024 Form 760 Instructions, Deductions from Income, Code 104
href: https://www.tax.virginia.gov/sites/default/files/vatax-pdf/2024-760-instructions.pdf#page=28
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ values:
- unemployment_compensation # Certification Number 37
- va_federal_state_employees_subtraction_person # Certification Number 39
- va_military_benefit_subtraction_person # Certification Number 60
- va_529_plan_deduction_person # Certification Number 34
- va_age_deduction_person # Certification Number 4
- taxable_social_security # Certification Number 5
- salt_refund_income # Form 760 Line 6 / Va. Code § 58.1-322.02(5)
Expand Down
109 changes: 109 additions & 0 deletions policyengine_us/tests/core/test_va_529_invariants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""Relational checks for Virginia's owner-level 529 deduction.

Va. Code § 58.1-322.03(7) bounds the owner's deduction and exempts owners
aged 70 or older from the cap. Form 760 lines 9, 13 and 15 place it after
VAGI. Paired returns check these relationships without inputting VAGI or
taxable income, or reproducing the deduction formula as a test oracle.
"""

import numpy as np
import pytest

from policyengine_us import Simulation


# Mixed rows exercise the age boundary, cap boundary, and account counts in
# one vectorized calculation. Each return also has a contributing spouse
# and a dependent who contributes to their own account.
OWNER_CASES = (
(69, 0, 0),
(69, 0, 1),
(69, 3_999, 1),
(69, 4_000, 1),
(69, 4_001, 1),
(69, 10_000, 1),
(69, 10_000, 2),
(69, 4_001, 0),
(70, 0, 1),
(70, 4_001, 1),
(70, 10_000, 2),
(72, 10_000, 1),
)


def _owner_situation(year):
situation = {
entity: {}
for entity in ("people", "tax_units", "spm_units", "families", "households")
}
for index, (age, contribution, accounts) in enumerate(OWNER_CASES):
for contributes in (True, False):
unit = f"unit_{index}_{contributes}"
head, spouse, dependent = (
f"head_{unit}",
f"spouse_{unit}",
f"dependent_{unit}",
)
for name, person_age, income, amount, count in (
(head, age, 60_000, contribution, accounts),
(spouse, 40, 40_000, 6_000, 1),
(dependent, 10, 0, 3_000, 1),
):
situation["people"][name] = {
"age": {year: person_age},
"employment_income": {year: income},
"investment_in_529_plan_indv": {year: amount if contributes else 0},
"count_529_contribution_beneficiaries": {year: count},
}
members = [head, spouse, dependent]
for entity in ("tax_units", "spm_units", "families", "households"):
situation[entity][unit] = {"members": members}
situation["households"][unit]["state_code"] = {year: "VA"}
return situation


@pytest.mark.parametrize("year", [2024, 2026])
def test_va_529_owner_bounds_aggregation_and_taxable_income_invariants(year):
situation = _owner_situation(year)
simulation = Simulation(situation=situation)

amounts = simulation.calculate("investment_in_529_plan_indv", year)
ages = simulation.calculate("age", year)
accounts = simulation.calculate("count_529_contribution_beneficiaries", year)
dependent = simulation.calculate("is_tax_unit_dependent", year)
deduction = simulation.calculate("va_529_plan_deduction_person", year)
total = simulation.calculate("va_529_plan_deduction", year)

# Each owner keeps their deduction, bounded by their own contribution.
assert np.all(deduction >= 0)
assert np.all(deduction <= amounts)
np.testing.assert_array_equal(deduction[dependent], 0)
younger_owner = (ages < 70) & ~dependent
assert np.all(deduction[younger_owner] <= 4_000 * accounts[younger_owner])
uncapped_owner = (ages >= 70) & ~dependent
np.testing.assert_array_equal(deduction[uncapped_owner], amounts[uncapped_owner])
below_cap = younger_owner & (amounts <= 4_000 * accounts)
np.testing.assert_array_equal(deduction[below_cap], amounts[below_cap])

# Aggregation retains each tax unit's own owners; no rows leak together.
# Core preserves situation dictionary insertion order: each tax unit has
# head/spouse/dependent rows, and contributing/zero-contribution units pair.
np.testing.assert_array_equal(total, deduction.reshape(-1, 3).sum(axis=1))

# Moving Form 760 code 104 below VAGI leaves both VAGI measures unchanged
# and reduces taxable income once, dollar for dollar. Income is high
# enough here that the taxable-income floor cannot bind.
for variable in ("va_agi_person", "va_agi"):
# Paired returns differ only in the contribution leaf input.
result = simulation.calculate(variable, year).reshape(len(OWNER_CASES), 2, -1)
np.testing.assert_array_equal(
result[:, 0],
result[:, 1],
)
taxable = simulation.calculate("va_taxable_income", year).reshape(-1, 2)
np.testing.assert_allclose(
taxable[:, 1] - taxable[:, 0],
total.reshape(-1, 2)[:, 0],
rtol=0,
atol=0.01,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Form 760 line-by-line cases for the Commonwealth Savers (529) deduction.
# The deduction is code 104 on Schedule ADJ line 8, totaled on Schedule ADJ
# line 9 and entered on Form 760 line 13, a deduction from Virginia adjusted
# gross income (line 9). It is not one of the subtractions on lines 4-7.
# Sources: 2024 Form 760 (PDF p. 1), 2024 Form 760 instructions (PDF pp. 17,
# 18, 28), Va. Code § 58.1-322.03(7).

- name: Form 760 lines 1-15, the 529 deduction is taken on line 13 after VAGI
period: 2024
absolute_error_margin: 0.01
input:
people:
person1:
age: 45
employment_income: 60_000
investment_in_529_plan_indv: 5_000
count_529_contribution_beneficiaries: 1
person2:
age: 45
employment_income: 40_000
tax_units:
tax_unit:
members: [person1, person2]
households:
household:
members: [person1, person2]
state_code: VA
output:
# Line 1 federal AGI 100_000; lines 2-8 zero; line 9 VAGI 100_000.
va_agi: 100_000
# Line 11 standard deduction (Filing Status 2) 17_000; line 12 exemptions
# 2 * 930 = 1_860; line 13 deductions, code 104: min(5_000, 4_000) = 4_000.
va_529_plan_deduction: 4_000
# Line 15 = 100_000 - (17_000 + 1_860 + 4_000) = 77_140.
va_taxable_income: 77_140
# Line 16 = 720 + 5.75% * (77_140 - 17_000) = 4_178.05.
va_income_tax_before_non_refundable_credits: 4_178.05

- name: Spouse Tax Adjustment worksheet uses each spouse's VAGI before the 529 deduction
period: 2024
absolute_error_margin: 0.01
input:
people:
person1:
age: 45
employment_income: 60_000
person2:
age: 45
employment_income: 12_000
investment_in_529_plan_indv: 4_000
count_529_contribution_beneficiaries: 1
tax_units:
tax_unit:
members: [person1, person2]
households:
household:
members: [person1, person2]
state_code: VA
output:
# Separate VAGI worksheet: line 11 FAGI 60_000 / 12_000; lines 14-17 hold
# the subtractions of Form 760 lines 4-7, which exclude line 13
# deductions; line 19 separate VAGI 60_000 / 12_000.
va_agi_person: [60_000, 12_000]
# Form 760 line 15 = 72_000 - 17_000 - 1_860 - 4_000 = 49_140.
va_taxable_income: 49_140
# STA worksheet:
# Line 3: 60_000 - 930 = 59_070; 12_000 - 930 = 11_070.
# Line 4: 49_140. Line 5: 11_070. Line 6: 49_140 - 11_070 = 38_070.
# Line 7: 49_140 / 2 = 24_570.
# Line 8: tax on 11_070 = 120 + 5% * 6_070 = 423.50.
# Line 9: tax on 38_070 = 720 + 5.75% * 21_070 = 1_931.525.
# Line 10: 2_355.025.
# Line 11: tax on 49_140 = 720 + 5.75% * 32_140 = 2_568.05.
# Line 12: 2_568.05 - 2_355.025 = 213.025 (below the 259 cap).
va_spouse_tax_adjustment: 213.025

- name: The filing threshold on line 9 is tested before the 529 deduction
period: 2024
absolute_error_margin: 0.01
input:
people:
person1:
age: 30
employment_income: 13_000
investment_in_529_plan_indv: 2_000
count_529_contribution_beneficiaries: 1
tax_units:
tax_unit:
members: [person1]
households:
household:
members: [person1]
state_code: VA
output:
# Line 9 VAGI 13_000 is not below the 11_950 Filing Status 1 threshold.
va_agi: 13_000
va_must_file: true
# Line 15 = 13_000 - 8_500 - 930 - 2_000 = 1_570; tax = 2% * 1_570.
va_taxable_income: 1_570
va_income_tax_before_non_refundable_credits: 31.40
Loading
Loading