diff --git a/changelog.d/fix-va-sta-social-security.fixed.md b/changelog.d/fix-va-sta-social-security.fixed.md new file mode 100644 index 00000000000..72754de3a56 --- /dev/null +++ b/changelog.d/fix-va-sta-social-security.fixed.md @@ -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. diff --git a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/integration.yaml index 9eea69a76ae..e2f9869d1cb 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/integration.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/integration.yaml @@ -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 diff --git a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/va_agi_person.yaml b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/va_agi_person.yaml index bdb25acb603..b27b791a35c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/va_agi_person.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/va_agi_person.yaml @@ -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] @@ -23,12 +24,13 @@ 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] @@ -36,6 +38,52 @@ 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: diff --git a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/va_subtractions_person.yaml b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/va_subtractions_person.yaml new file mode 100644 index 00000000000..9e8338c315d --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/va_subtractions_person.yaml @@ -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] diff --git a/policyengine_us/variables/gov/states/va/tax/income/va_agi_person.py b/policyengine_us/variables/gov/states/va/tax/income/va_agi_person.py index 469082775e1..bbf929fa323 100644 --- a/policyengine_us/variables/gov/states/va/tax/income/va_agi_person.py +++ b/policyengine_us/variables/gov/states/va/tax/income/va_agi_person.py @@ -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 diff --git a/policyengine_us/variables/gov/states/va/tax/income/va_subtractions_person.py b/policyengine_us/variables/gov/states/va/tax/income/va_subtractions_person.py new file mode 100644 index 00000000000..83c1aa3875e --- /dev/null +++ b/policyengine_us/variables/gov/states/va/tax/income/va_subtractions_person.py @@ -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 + )