Skip to content
Open
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-sta-social-security.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Corrected the Virginia per-person adjusted gross income used by the Spouse Tax Adjustment to attribute each Virginia subtraction (Social Security, railroad retirement, unemployment, US government interest, military and disability subtractions, and the age deduction) to the spouse who received the income, preventing the adjustment from being wrongly granted to couples where one spouse only has Virginia-exempt income.
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,89 @@
va_itemized_deductions: 37_049.81
va_income_tax: 18175.545

- name: MFJ both age 70 - Social Security recipient spouse does not trigger Spouse Tax Adjustment
# Regression test for https://github.com/PolicyEngine/policyengine-us/issues/8743.
# Virginia exempts Social Security, so a spouse whose only income is Social
# Security has no separate Virginia taxable income and the couple does not
# qualify for the Spouse Tax Adjustment. Confirmed against the Virginia Form
# 760 Spouse Tax Adjustment worksheet, TaxAct, and TAXSIM-35.
absolute_error_margin: 0.01
period: 2025
input:
people:
person1:
age: 70
employment_income: 125_714
taxable_interest_income: 255.5
social_security_retirement: 14_391.5
person2:
age: 70
taxable_interest_income: 255.5
social_security_retirement: 14_391.5
tax_units:
tax_unit:
members: [person1, person2]
va_rebate: 0
households:
household:
members: [person1, person2]
state_code: VA
output:
va_spouse_tax_adjustment_eligible: false
va_spouse_tax_adjustment: 0
va_income_tax: 5_795.24

- name: MFJ - spouse with only Virginia-exempt unemployment does not trigger Spouse Tax Adjustment
# Virginia subtracts unemployment compensation (separate-VAGI worksheet, Line
# 17), so a spouse whose only income is unemployment has no separate Virginia
# taxable income and the couple does not qualify for the Spouse Tax Adjustment.
absolute_error_margin: 0.01
period: 2025
input:
people:
person1:
age: 45
employment_income: 125_714
person2:
age: 45
unemployment_compensation: 25_000
tax_units:
tax_unit:
members: [person1, person2]
va_rebate: 0
households:
household:
members: [person1, person2]
state_code: VA
output:
va_spouse_tax_adjustment_eligible: false
va_spouse_tax_adjustment: 0

- name: MFJ - two earners still receive the Spouse Tax Adjustment (no over-correction)
# Guard against over-correcting: a genuinely two-earner couple (no exempt
# income) must still qualify for and receive the Spouse Tax Adjustment.
absolute_error_margin: 0.01
period: 2025
input:
people:
person1:
age: 45
employment_income: 100_000
person2:
age: 45
employment_income: 40_000
tax_units:
tax_unit:
members: [person1, person2]
va_rebate: 0
households:
household:
members: [person1, person2]
state_code: VA
output:
va_spouse_tax_adjustment_eligible: true
va_spouse_tax_adjustment: 257.5

- name: Joint couple with one child, 81k income each
absolute_error_margin: 2
period: 2023
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
- name: Attributing the state adjusted gross income based on federal AGI
- name: Each person's VAGI is their federal AGI less their own Virginia subtractions
period: 2021
input:
people:
person1:
adjusted_gross_income_person: 10_000
va_subtractions_person: 2_500
person2:
adjusted_gross_income_person: 30_000
va_subtractions_person: 7_500
tax_units:
tax_unit:
members: [person1, person2]
va_subtractions: 10_000
households:
household:
members: [person1, person2]
Expand All @@ -23,19 +24,66 @@
people:
person1:
adjusted_gross_income_person: 10_000
va_subtractions_person: 2_000
person2:
adjusted_gross_income_person: 0
va_subtractions_person: 0
tax_units:
tax_unit:
members: [person1, person2]
va_subtractions: 2_000
households:
household:
members: [person1, person2]
state_code: VA
output:
va_agi_person: [8_000, 0]

- name: Social Security subtraction applied directly to the recipient, not prorated
period: 2025
input:
people:
person1:
age: 70
employment_income: 100_000
person2:
age: 70
social_security_retirement: 20_000
tax_units:
tax_unit:
members: [person1, person2]
households:
household:
members: [person1, person2]
state_code: VA
output:
# person2's $17,000 taxable Social Security is exempt in Virginia and is
# subtracted from her own Virginia AGI (not prorated to person1), so her
# separate VAGI is 0. The two columns still sum to the tax unit's VAGI.
va_agi_person: [100_000, 0]

- name: Unemployment subtraction applied directly to the recipient, not prorated
period: 2025
input:
people:
person1:
age: 45
employment_income: 100_000
person2:
age: 45
unemployment_compensation: 15_000
tax_units:
tax_unit:
members: [person1, person2]
households:
household:
members: [person1, person2]
state_code: VA
output:
# Virginia subtracts unemployment compensation; person2's $15,000 is
# attributed to her, so her separate VAGI is 0 rather than a prorated
# share of the subtraction. The columns still sum to the tax unit's VAGI.
va_agi_person: [100_000, 0]

- name: Age deduction applied directly to eligible person, not prorated
period: 2021
input:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
- name: Unemployment compensation is attributed to the recipient spouse
period: 2025
input:
people:
person1:
age: 45
employment_income: 100_000
person2:
age: 45
unemployment_compensation: 15_000
tax_units:
tax_unit:
members: [person1, person2]
households:
household:
members: [person1, person2]
state_code: VA
output:
va_subtractions_person: [0, 15_000]

- name: Disability income subtraction is attributed to the recipient spouse
period: 2025
input:
people:
person1:
age: 45
employment_income: 100_000
person2:
age: 45
disability_benefits: 15_000
tax_units:
tax_unit:
members: [person1, person2]
households:
household:
members: [person1, person2]
state_code: VA
output:
va_subtractions_person: [0, 15_000]

- name: Military basic pay subtraction is attributed to the service member
period: 2025
input:
people:
person1:
age: 45
military_basic_pay: 15_000
employment_income: 15_000
person2:
age: 45
employment_income: 50_000
tax_units:
tax_unit:
members: [person1, person2]
households:
household:
members: [person1, person2]
state_code: VA
output:
va_subtractions_person: [15_000, 0]
24 changes: 12 additions & 12 deletions policyengine_us/variables/gov/states/va/tax/income/va_agi_person.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,18 @@ class va_agi_person(Variable):

def formula(person, period, parameters):
person_fagi = person("adjusted_gross_income_person", period)
# Apply person-level age deduction directly
age_deduction = person("va_age_deduction_person", period)
# Prorate non-age subtractions by federal AGI share
total_subtractions = person.tax_unit("va_subtractions", period)
total_age_deduction = person.tax_unit("va_age_deduction", period)
non_age_subtractions = max_(total_subtractions - total_age_deduction, 0)
# Virginia's "Worksheet for Determining Separate Virginia Adjusted Gross
# Income" (Form 760 instructions) attributes each subtraction to the
# spouse who received the underlying income (va_subtractions_person),
# then sets each spouse's separate VAGI. This matters for the Spouse Tax
# Adjustment: a spouse whose only income is Virginia-exempt (e.g. Social
# Security, railroad retirement, unemployment) has no separate Virginia
# taxable income, so the couple should not qualify for the adjustment.
person_subtractions = person("va_subtractions_person", period)
# Additions are only defined at the tax-unit level, so prorate them by
# federal AGI share.
additions = person.tax_unit("va_additions", period)
total_federal_agi = person.tax_unit.sum(person_fagi)
prorate = where(total_federal_agi > 0, person_fagi / total_federal_agi, 0)
person_non_age_subtractions = non_age_subtractions * prorate
# Prorate additions the same way
additions = person.tax_unit("va_additions", period)
person_additions = additions * prorate
return (
person_fagi + person_additions - age_deduction - person_non_age_subtractions
)
return person_fagi + person_additions - person_subtractions
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
from policyengine_us.model_api import *


class va_subtractions_person(Variable):
value_type = float
entity = Person
label = "Virginia adjusted gross income subtractions attributed to each person"
unit = USD
definition_period = YEAR
defined_for = StateCode.VA
reference = "https://www.tax.virginia.gov/sites/default/files/vatax-pdf/2022-760-instructions.pdf#page=19"

def formula(person, period, parameters):
# Virginia's "Worksheet for Determining Separate Virginia Adjusted Gross
# Income" (Form 760 instructions, STEP 2, Lines 14-17) attributes each
# subtraction to the spouse who received the underlying income. This
# builds each person's share so the per-person amounts sum to the tax
# unit's va_subtractions, which matters for the Spouse Tax Adjustment and
# the per-person Virginia EITC split.
#
# The components below mirror, item by item, the subtractions listed in
# gov.states.va.tax.income.subtractions.subtractions; keep them in sync.
tax_unit = person.tax_unit
is_head_or_spouse = person("is_tax_unit_head_or_spouse", period)
p = parameters(period).gov.states.va.tax.income.subtractions

# Person-level income exclusions: the income (and therefore the
# subtraction) belongs to the individual who received it.
age_deduction = person("va_age_deduction_person", period) # Line 14
social_security = person("taxable_social_security", period) # Line 15
railroad = person("railroad_benefits", period) # Line 15 (Tier 1 Railroad)
unemployment = person("unemployment_compensation", period) # Line 17
us_govt_interest = person("us_govt_interest_person", period) # Line 17

# Military basic pay subtraction: phases in then out per person (mirrors
# va_military_basic_pay_subtraction).
military_pay = person("military_basic_pay", period)
military_basic_pay_subtraction = (
where(
military_pay < p.military_basic_pay.threshold,
military_pay,
max_(0, 2 * p.military_basic_pay.threshold - military_pay),
)
* is_head_or_spouse
)
# Disability income subtraction, capped per person (mirrors
# va_disability_income_subtraction).
disability = (
min_(person("disability_benefits", period), p.disability_income.amount)
* is_head_or_spouse
)
# Federal/state employees subtraction (mirrors
# va_federal_state_employees_subtraction, which gates on the disability
# income amount parameter).
employment_income = person("irs_employment_income", period)
federal_state_employees = (
where(
employment_income > p.disability_income.amount,
0,
person("state_or_federal_salary", period),
)
* is_head_or_spouse
)
# Military benefit subtraction, capped and optionally age-gated (mirrors
# va_military_benefit_subtraction).
military_benefit = min_(
person("military_retirement_pay", period), p.military_benefit.amount
)
if p.military_benefit.availability:
military_benefit = military_benefit * (
person("age", period) >= p.military_benefit.age_threshold
)
military_benefit = military_benefit * is_head_or_spouse

# National Guard subtraction caps the sum of military service income, so
# allocate the (capped) tax-unit amount by each person's share.
national_guard_total = tax_unit("va_national_guard_subtraction", period)
military_service_income = person("military_service_income", period)
total_military_service_income = tax_unit.sum(military_service_income)
national_guard_share = where(
total_military_service_income > 0,
military_service_income / total_military_service_income,
0,
)
national_guard = national_guard_total * national_guard_share

# The 529 deduction is a household contribution, not a person's income,
# so it cannot make a spouse's separate VAGI exceed their own income;
# prorate it by federal AGI share.
plan_529_total = tax_unit("va_529_plan_deduction", period)
person_fagi = person("adjusted_gross_income_person", period)
total_federal_agi = tax_unit.sum(person_fagi)
federal_agi_share = where(
total_federal_agi > 0, person_fagi / total_federal_agi, 0
)
plan_529 = plan_529_total * federal_agi_share

return (
age_deduction
+ social_security
+ railroad
+ unemployment
+ us_govt_interest
+ military_basic_pay_subtraction
+ disability
+ federal_state_employees
+ military_benefit
+ national_guard
+ plan_529
)
Loading