From 50b1316236fd15de36169552dc0aa1ae1603716f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 07:08:28 -0400 Subject: [PATCH 1/4] Allocate Virginia's 529 deduction to the owner of record and take it after VAGI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Commonwealth Savers (529) deduction is a deduction from Virginia adjusted gross income (Va. Code § 58.1-322.03(7); Form 760 instructions, deduction code 104, entered on Form 760 line 13), not a subtraction in computing VAGI. Code 34, which the subtraction list cited, is for distributions and refunds. Only the owner of record may claim the deduction, and an owner who has attained age 70 deducts the full contribution. Co-Authored-By: Claude Opus 5.5 --- .../fix-va-529-owner-taxable-income.fixed.md | 1 + .../subtractions/plan_529/age_threshold.yaml | 12 ++ .../tax/income/subtractions/plan_529/cap.yaml | 15 +- .../tax/income/subtractions/subtractions.yaml | 1 - .../deductions/plan_529/integration.yaml | 100 +++++++++++ .../plan_529/va_529_plan_deduction.yaml | 155 ++++++++++++++++++ .../subtractions/va_529_plan_deduction.yaml | 43 ----- .../plan_529/va_529_plan_deduction.py | 20 +++ .../plan_529/va_529_plan_deduction_person.py | 30 ++++ .../subtractions/va_529_plan_deduction.py | 25 --- .../va_529_plan_deduction_person.py | 26 --- .../states/va/tax/income/va_taxable_income.py | 11 +- 12 files changed, 333 insertions(+), 106 deletions(-) create mode 100644 changelog.d/fix-va-529-owner-taxable-income.fixed.md create mode 100644 policyengine_us/parameters/gov/states/va/tax/income/subtractions/plan_529/age_threshold.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/va/tax/income/deductions/plan_529/integration.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/states/va/tax/income/deductions/plan_529/va_529_plan_deduction.yaml delete mode 100644 policyengine_us/tests/policy/baseline/gov/states/va/tax/income/subtractions/va_529_plan_deduction.yaml create mode 100644 policyengine_us/variables/gov/states/va/tax/income/deductions/plan_529/va_529_plan_deduction.py create mode 100644 policyengine_us/variables/gov/states/va/tax/income/deductions/plan_529/va_529_plan_deduction_person.py delete mode 100644 policyengine_us/variables/gov/states/va/tax/income/subtractions/va_529_plan_deduction.py delete mode 100644 policyengine_us/variables/gov/states/va/tax/income/subtractions/va_529_plan_deduction_person.py diff --git a/changelog.d/fix-va-529-owner-taxable-income.fixed.md b/changelog.d/fix-va-529-owner-taxable-income.fixed.md new file mode 100644 index 00000000000..2d360ee3fbc --- /dev/null +++ b/changelog.d/fix-va-529-owner-taxable-income.fixed.md @@ -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. diff --git a/policyengine_us/parameters/gov/states/va/tax/income/subtractions/plan_529/age_threshold.yaml b/policyengine_us/parameters/gov/states/va/tax/income/subtractions/plan_529/age_threshold.yaml new file mode 100644 index 00000000000..7e80df7df22 --- /dev/null +++ b/policyengine_us/parameters/gov/states/va/tax/income/subtractions/plan_529/age_threshold.yaml @@ -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 diff --git a/policyengine_us/parameters/gov/states/va/tax/income/subtractions/plan_529/cap.yaml b/policyengine_us/parameters/gov/states/va/tax/income/subtractions/plan_529/cap.yaml index e28c0885689..33e7813d708 100644 --- a/policyengine_us/parameters/gov/states/va/tax/income/subtractions/plan_529/cap.yaml +++ b/policyengine_us/parameters/gov/states/va/tax/income/subtractions/plan_529/cap.yaml @@ -1,7 +1,4 @@ -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. +description: Virginia caps the Commonwealth Savers account contributions deduction at this amount per account per year. values: 2021-01-01: 4_000 metadata: @@ -9,9 +6,7 @@ metadata: 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 diff --git a/policyengine_us/parameters/gov/states/va/tax/income/subtractions/subtractions.yaml b/policyengine_us/parameters/gov/states/va/tax/income/subtractions/subtractions.yaml index 4e8db1e3717..2e966e826a7 100644 --- a/policyengine_us/parameters/gov/states/va/tax/income/subtractions/subtractions.yaml +++ b/policyengine_us/parameters/gov/states/va/tax/income/subtractions/subtractions.yaml @@ -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) diff --git a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/deductions/plan_529/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/deductions/plan_529/integration.yaml new file mode 100644 index 00000000000..79e8ca5bdd2 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/deductions/plan_529/integration.yaml @@ -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 diff --git a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/deductions/plan_529/va_529_plan_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/deductions/plan_529/va_529_plan_deduction.yaml new file mode 100644 index 00000000000..05568e40746 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/deductions/plan_529/va_529_plan_deduction.yaml @@ -0,0 +1,155 @@ +- name: Contribution below cap with one beneficiary + period: 2024 + input: + state_code: VA + investment_in_529_plan_indv: 3_000 + count_529_contribution_beneficiaries: 1 + output: + va_529_plan_deduction: 3_000 + +- name: Contribution above cap with one beneficiary + period: 2024 + input: + state_code: VA + investment_in_529_plan_indv: 6_000 + count_529_contribution_beneficiaries: 1 + output: + va_529_plan_deduction: 4_000 + +- name: Two beneficiaries increases the cap + period: 2024 + input: + people: + person1: + investment_in_529_plan_indv: 7_000 + count_529_contribution_beneficiaries: 2 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: VA + output: + va_529_plan_deduction: 7_000 + +- name: Zero contributions + period: 2024 + input: + state_code: VA + investment_in_529_plan_indv: 0 + count_529_contribution_beneficiaries: 0 + output: + va_529_plan_deduction: 0 + +- name: Only the owner of record deducts the contribution, not both spouses by income share + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + employment_income: 60_000 + person2: + age: 45 + employment_income: 40_000 + investment_in_529_plan_indv: 3_000 + count_529_contribution_beneficiaries: 1 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: VA + output: + # 2024 Form 760 instructions, code 104: "Only the owner of record for an + # account may claim a deduction for contributions made." + va_529_plan_deduction_person: [0, 3_000] + va_529_plan_deduction: 3_000 + +- name: An owner age 70 or older deducts the full contribution + period: 2024 + input: + people: + person1: + age: 72 + investment_in_529_plan_indv: 10_000 + count_529_contribution_beneficiaries: 1 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: VA + output: + # Va. Code § 58.1-322.03(7)(b); code 104: "if you are age 70 or older on or + # before December 31 of the taxable year, you may deduct the entire amount + # contributed during the taxable year." + va_529_plan_deduction: 10_000 + +- name: An owner under 70 is held to the per-account cap + period: 2024 + input: + people: + person1: + age: 69 + investment_in_529_plan_indv: 10_000 + count_529_contribution_beneficiaries: 1 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: VA + output: + va_529_plan_deduction: 4_000 + +- name: Each spouse's own accounts carry their own cap + period: 2024 + input: + people: + person1: + age: 45 + investment_in_529_plan_indv: 6_000 + count_529_contribution_beneficiaries: 1 + person2: + age: 71 + investment_in_529_plan_indv: 6_000 + count_529_contribution_beneficiaries: 1 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: VA + output: + # Person 1 is capped at 4_000 for one account; person 2 is 70 or older and + # deducts the full 6_000. + va_529_plan_deduction_person: [4_000, 6_000] + va_529_plan_deduction: 10_000 + +- name: A dependent's account is not deducted on the parents' return + period: 2024 + input: + people: + person1: + age: 45 + person2: + age: 16 + is_tax_unit_dependent: true + investment_in_529_plan_indv: 1_000 + count_529_contribution_beneficiaries: 1 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: VA + output: + va_529_plan_deduction_person: [0, 0] + va_529_plan_deduction: 0 diff --git a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/subtractions/va_529_plan_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/subtractions/va_529_plan_deduction.yaml deleted file mode 100644 index 15f3a20889f..00000000000 --- a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/subtractions/va_529_plan_deduction.yaml +++ /dev/null @@ -1,43 +0,0 @@ -- name: Contribution below cap with one beneficiary - period: 2024 - input: - state_code: VA - investment_in_529_plan_indv: 3_000 - count_529_contribution_beneficiaries: 1 - output: - va_529_plan_deduction: 3_000 - -- name: Contribution above cap with one beneficiary - period: 2024 - input: - state_code: VA - investment_in_529_plan_indv: 6_000 - count_529_contribution_beneficiaries: 1 - output: - va_529_plan_deduction: 4_000 - -- name: Two beneficiaries increases the cap - period: 2024 - input: - people: - person1: - investment_in_529_plan_indv: 7_000 - count_529_contribution_beneficiaries: 2 - tax_units: - tax_unit: - members: [person1] - households: - household: - members: [person1] - state_code: VA - output: - va_529_plan_deduction: 7_000 - -- name: Zero contributions - period: 2024 - input: - state_code: VA - investment_in_529_plan_indv: 0 - count_529_contribution_beneficiaries: 0 - output: - va_529_plan_deduction: 0 diff --git a/policyengine_us/variables/gov/states/va/tax/income/deductions/plan_529/va_529_plan_deduction.py b/policyengine_us/variables/gov/states/va/tax/income/deductions/plan_529/va_529_plan_deduction.py new file mode 100644 index 00000000000..bf6205ce8bd --- /dev/null +++ b/policyengine_us/variables/gov/states/va/tax/income/deductions/plan_529/va_529_plan_deduction.py @@ -0,0 +1,20 @@ +from policyengine_us.model_api import * + + +class va_529_plan_deduction(Variable): + value_type = float + entity = TaxUnit + label = "Virginia deduction for contributions to Commonwealth Savers accounts" + unit = USD + definition_period = YEAR + reference = ( + # Va. Code § 58.1-322.03(7): a deduction from Virginia adjusted gross + # income in computing Virginia taxable income. + "https://law.lis.virginia.gov/vacode/title58.1/chapter3/section58.1-322.03/", + # 2024 Form 760 instructions, deduction code 104 (Schedule ADJ line 8, + # carried to Form 760 line 13) + "https://www.tax.virginia.gov/sites/default/files/vatax-pdf/2024-760-instructions.pdf#page=28", + ) + defined_for = StateCode.VA + + adds = ["va_529_plan_deduction_person"] diff --git a/policyengine_us/variables/gov/states/va/tax/income/deductions/plan_529/va_529_plan_deduction_person.py b/policyengine_us/variables/gov/states/va/tax/income/deductions/plan_529/va_529_plan_deduction_person.py new file mode 100644 index 00000000000..7f53a6614a0 --- /dev/null +++ b/policyengine_us/variables/gov/states/va/tax/income/deductions/plan_529/va_529_plan_deduction_person.py @@ -0,0 +1,30 @@ +from policyengine_us.model_api import * + + +class va_529_plan_deduction_person(Variable): + value_type = float + entity = Person + label = "Virginia Commonwealth Savers account deduction claimed by each owner" + unit = USD + definition_period = YEAR + reference = ( + # Va. Code § 58.1-322.03(7)(a)-(b) + "https://law.lis.virginia.gov/vacode/title58.1/chapter3/section58.1-322.03/", + # 2024 Form 760 instructions, deduction code 104 + "https://www.tax.virginia.gov/sites/default/files/vatax-pdf/2024-760-instructions.pdf#page=28", + ) + defined_for = StateCode.VA + + def formula(person, period, parameters): + p = parameters(period).gov.states.va.tax.income.subtractions.plan_529 + # Only the owner of record of an account may claim the deduction, so + # each filer deducts their own contributions. A dependent's account is + # claimed on the dependent's own return. + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + contributions = person("investment_in_529_plan_indv", period) + accounts = person("count_529_contribution_beneficiaries", period) + capped = min_(contributions, p.cap * accounts) + # An owner who has attained age 70 may deduct the full amount + # contributed during the year. + age_exempt = person("age", period) >= p.age_threshold + return head_or_spouse * where(age_exempt, contributions, capped) diff --git a/policyengine_us/variables/gov/states/va/tax/income/subtractions/va_529_plan_deduction.py b/policyengine_us/variables/gov/states/va/tax/income/subtractions/va_529_plan_deduction.py deleted file mode 100644 index 269b87198be..00000000000 --- a/policyengine_us/variables/gov/states/va/tax/income/subtractions/va_529_plan_deduction.py +++ /dev/null @@ -1,25 +0,0 @@ -from policyengine_us.model_api import * - - -class va_529_plan_deduction(Variable): - value_type = float - entity = TaxUnit - label = "Virginia deduction for contributions to 529 plans" - unit = USD - definition_period = YEAR - reference = ( - "https://www.tax.virginia.gov/deductions", - "https://law.lis.virginia.gov/vacodefull/title58.1/chapter3/article2/", - ) - defined_for = StateCode.VA - - def formula(tax_unit, period, parameters): - p = parameters(period).gov.states.va.tax.income.subtractions.plan_529 - contributions = tax_unit("investment_in_529_plan", period) - beneficiaries = add( - tax_unit, - period, - ["count_529_contribution_beneficiaries"], - ) - cap = p.cap * beneficiaries - return min_(contributions, cap) diff --git a/policyengine_us/variables/gov/states/va/tax/income/subtractions/va_529_plan_deduction_person.py b/policyengine_us/variables/gov/states/va/tax/income/subtractions/va_529_plan_deduction_person.py deleted file mode 100644 index 6ce145c89e9..00000000000 --- a/policyengine_us/variables/gov/states/va/tax/income/subtractions/va_529_plan_deduction_person.py +++ /dev/null @@ -1,26 +0,0 @@ -from policyengine_us.model_api import * - - -class va_529_plan_deduction_person(Variable): - value_type = float - entity = Person - label = "Virginia 529 plan deduction allocated to each person" - defined_for = StateCode.VA - unit = USD - definition_period = YEAR - reference = "https://law.lis.virginia.gov/vacodefull/title58.1/chapter3/article2/" - - def formula(person, period, parameters): - # The 529 deduction is a household contribution, not a person's income, - # so prorate it by federal AGI share. When the household has no positive - # federal AGI, assign it to the head so the per-person amounts still sum - # to the household deduction. - total = person.tax_unit("va_529_plan_deduction", period) - person_fagi = person("adjusted_gross_income_person", period) - total_federal_agi = person.tax_unit.sum(person_fagi) - share = where( - total_federal_agi > 0, - person_fagi / total_federal_agi, - person("is_tax_unit_head", period), - ) - return total * share diff --git a/policyengine_us/variables/gov/states/va/tax/income/va_taxable_income.py b/policyengine_us/variables/gov/states/va/tax/income/va_taxable_income.py index 47c1b126cca..d75f54a5fe6 100644 --- a/policyengine_us/variables/gov/states/va/tax/income/va_taxable_income.py +++ b/policyengine_us/variables/gov/states/va/tax/income/va_taxable_income.py @@ -10,6 +10,7 @@ class va_taxable_income(Variable): reference = ( "https://law.lis.virginia.gov/vacodefull/title58.1/chapter3/article2/", "https://www.tax.virginia.gov/sites/default/files/taxforms/individual-income-tax/2022/760-2022.pdf#page=1", + "https://www.tax.virginia.gov/sites/default/files/taxforms/individual-income-tax/2024/760-2024.pdf#page=1", ) defined_for = StateCode.VA @@ -25,7 +26,15 @@ def formula(tax_unit, period, parameters): ) # Virginia also allows the eligible educator expense deduction. educator_deduction = tax_unit("va_educator_expense_deduction", period) + # Commonwealth Savers (529) contributions are a deduction from Virginia + # adjusted gross income (Schedule ADJ code 104, Form 760 line 13), not + # a subtraction in computing it. + plan_529_deduction = tax_unit("va_529_plan_deduction", period) return max_( - agi - total_deductions - cdcc_expense_deduction - educator_deduction, + agi + - total_deductions + - cdcc_expense_deduction + - educator_deduction + - plan_529_deduction, 0, ) From 3fa0fd7063af3d708feca57bd15538d98f775e49 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 08:31:25 -0400 Subject: [PATCH 2/4] Reword the 529 owner comment as a modeling convention Co-Authored-By: Claude Opus 5.5 --- .../deductions/plan_529/va_529_plan_deduction_person.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/policyengine_us/variables/gov/states/va/tax/income/deductions/plan_529/va_529_plan_deduction_person.py b/policyengine_us/variables/gov/states/va/tax/income/deductions/plan_529/va_529_plan_deduction_person.py index 7f53a6614a0..7fce214f2e1 100644 --- a/policyengine_us/variables/gov/states/va/tax/income/deductions/plan_529/va_529_plan_deduction_person.py +++ b/policyengine_us/variables/gov/states/va/tax/income/deductions/plan_529/va_529_plan_deduction_person.py @@ -18,8 +18,10 @@ class va_529_plan_deduction_person(Variable): def formula(person, period, parameters): p = parameters(period).gov.states.va.tax.income.subtractions.plan_529 # Only the owner of record of an account may claim the deduction, so - # each filer deducts their own contributions. A dependent's account is - # claimed on the dependent's own return. + # each filer deducts the contributions to accounts they own; the model + # reads a person's 529 contributions as contributions to their own + # accounts. Dependents file their own returns, which this tax unit + # does not model, so dependent-owned accounts are excluded. head_or_spouse = person("is_tax_unit_head_or_spouse", period) contributions = person("investment_in_529_plan_indv", period) accounts = person("count_529_contribution_beneficiaries", period) From b4fb768014fda470c6bfc35fde0c27e9ac351b63 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 8 Oct 2026 11:25:47 -0400 Subject: [PATCH 3/4] test: pin Virginia 529 deduction at age 70 (#9875) --- .../deductions/plan_529/va_529_plan_deduction.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/deductions/plan_529/va_529_plan_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/deductions/plan_529/va_529_plan_deduction.yaml index 05568e40746..5cddd899160 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/deductions/plan_529/va_529_plan_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/deductions/plan_529/va_529_plan_deduction.yaml @@ -107,6 +107,16 @@ output: va_529_plan_deduction: 4_000 +- name: An owner aged exactly 70 deducts the full contribution + period: 2024 + input: + state_code: VA + age: 70 + investment_in_529_plan_indv: 10_000 + count_529_contribution_beneficiaries: 1 + output: + va_529_plan_deduction: 10_000 + - name: Each spouse's own accounts carry their own cap period: 2024 input: From 5324f5038f2e412177ac5f9a9ac65310e2460501 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 8 Oct 2026 20:16:41 -0400 Subject: [PATCH 4/4] test: verify Virginia 529 deduction invariants --- .../tax/income/subtractions/plan_529/cap.yaml | 1 + .../tests/core/test_va_529_invariants.py | 109 ++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 policyengine_us/tests/core/test_va_529_invariants.py diff --git a/policyengine_us/parameters/gov/states/va/tax/income/subtractions/plan_529/cap.yaml b/policyengine_us/parameters/gov/states/va/tax/income/subtractions/plan_529/cap.yaml index 33e7813d708..396ad65e9d3 100644 --- a/policyengine_us/parameters/gov/states/va/tax/income/subtractions/plan_529/cap.yaml +++ b/policyengine_us/parameters/gov/states/va/tax/income/subtractions/plan_529/cap.yaml @@ -1,3 +1,4 @@ +# 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 diff --git a/policyengine_us/tests/core/test_va_529_invariants.py b/policyengine_us/tests/core/test_va_529_invariants.py new file mode 100644 index 00000000000..d78be48a718 --- /dev/null +++ b/policyengine_us/tests/core/test_va_529_invariants.py @@ -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, + )