From a6a37ed95baa11560f2929e2bea022af673fc5ac Mon Sep 17 00:00:00 2001 From: PavelMakarchuk Date: Wed, 24 Jun 2026 12:49:25 -0400 Subject: [PATCH 1/3] Fix Virginia Spouse Tax Adjustment wrongly applied to Social Security recipients va_agi_person prorated all non-age subtractions (including the Social Security subtraction) by federal AGI share, shifting a Social-Security-only spouse's exempt benefits onto the higher-income spouse and inflating the first spouse's separate VAGI. This pushed couples over the Spouse Tax Adjustment eligibility threshold. Per Virginia's "Worksheet for Determining Separate Virginia Adjusted Gross Income" (Form 760 instructions, Lines 8 and 15), each spouse's own taxable Social Security is attributed to and subtracted from that spouse. Apply the taxable Social Security subtraction at the person level and prorate only the remaining subtractions; the per-person values still sum to the tax unit VAGI. Adds a regression integration test (the issue scenario) and a person-level va_agi_person test; both fail before the fix and pass after. Closes #8743 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../fix-va-sta-social-security.fixed.md | 1 + .../gov/states/va/tax/income/integration.yaml | 31 +++++++++++++++++++ .../states/va/tax/income/va_agi_person.yaml | 23 ++++++++++++++ .../gov/states/va/tax/income/va_agi_person.py | 31 ++++++++++++++++--- 4 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 changelog.d/fix-va-sta-social-security.fixed.md 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..cb71597a841 --- /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 subtract exempt Social Security benefits from the recipient spouse, preventing the adjustment from being wrongly granted to couples where one spouse only has Social Security 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..1fda1c7118c 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,37 @@ 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: 1 + 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: 0 + va_income_tax: 5_795 + - 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..f83208dcc0f 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 @@ -36,6 +36,29 @@ 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: Age deduction applied directly to eligible person, not prorated period: 2021 input: 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..6e28bae9175 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,39 @@ class va_agi_person(Variable): def formula(person, period, parameters): person_fagi = person("adjusted_gross_income_person", period) - # Apply person-level age deduction directly + # Apply person-level age deduction directly (separate VAGI worksheet, Line 14). age_deduction = person("va_age_deduction_person", period) - # Prorate non-age subtractions by federal AGI share + # Apply the Social Security / Tier 1 Railroad subtraction directly to the + # person who received the benefits (separate VAGI worksheet, Line 15). + # Virginia exempts federally taxable Social Security in full, so a spouse + # whose only income is Social Security has no separate Virginia taxable + # income. Prorating this subtraction by federal AGI share would instead + # shift it to a higher-income spouse and wrongly inflate this spouse's + # VAGI (affecting the Spouse Tax Adjustment). + social_security_subtraction = person("taxable_social_security", period) + total_social_security_subtraction = person.tax_unit( + "tax_unit_taxable_social_security", period + ) + # Prorate the remaining (non-age, non-Social-Security) 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) + prorated_subtractions = max_( + total_subtractions + - total_age_deduction + - total_social_security_subtraction, + 0, + ) 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 + person_prorated_subtractions = prorated_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 + person_fagi + + person_additions + - age_deduction + - social_security_subtraction + - person_prorated_subtractions ) From 22b9d4e8aa5aa8da8f514705c70c663ff4b1e175 Mon Sep 17 00:00:00 2001 From: PavelMakarchuk Date: Wed, 24 Jun 2026 13:11:26 -0400 Subject: [PATCH 2/3] Broaden VA per-person subtraction fix to unemployment and railroad; harden tests Self-review found the Social-Security-only fix left the same bug live for other Virginia-exempt income types. Virginia's separate-VAGI worksheet attributes all subtractions to the recipient spouse (Lines 14-17), so a spouse whose only income is unemployment or railroad retirement was likewise pushed into Spouse Tax Adjustment eligibility. Attribute every person-level Virginia subtraction (taxable Social Security, railroad retirement, unemployment compensation) plus the age deduction to the recipient spouse, and prorate only the remaining tax-unit-level subtractions by federal AGI share. The per-person values still sum to the tax-unit VAGI. Tests: add unemployment regression cases (integration + person-level), an over-correction guard confirming a genuine two-earner couple still receives the adjustment, and an explicit eligible=false assertion; tighten the integration margin so a zero adjustment is asserted exactly. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../fix-va-sta-social-security.fixed.md | 2 +- .../gov/states/va/tax/income/integration.yaml | 56 ++++++++++++++++++- .../states/va/tax/income/va_agi_person.yaml | 23 ++++++++ .../gov/states/va/tax/income/va_agi_person.py | 46 +++++++-------- 4 files changed, 101 insertions(+), 26 deletions(-) diff --git a/changelog.d/fix-va-sta-social-security.fixed.md b/changelog.d/fix-va-sta-social-security.fixed.md index cb71597a841..6f68d439415 100644 --- a/changelog.d/fix-va-sta-social-security.fixed.md +++ b/changelog.d/fix-va-sta-social-security.fixed.md @@ -1 +1 @@ -Corrected the Virginia per-person adjusted gross income used by the Spouse Tax Adjustment to subtract exempt Social Security benefits from the recipient spouse, preventing the adjustment from being wrongly granted to couples where one spouse only has Social Security income. +Corrected the Virginia per-person adjusted gross income used by the Spouse Tax Adjustment to attribute exempt Social Security, railroad retirement, and unemployment subtractions to the recipient spouse, 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 1fda1c7118c..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 @@ -146,7 +146,7 @@ # 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: 1 + absolute_error_margin: 0.01 period: 2025 input: people: @@ -168,8 +168,60 @@ 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 - va_income_tax: 5_795 + +- 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 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 f83208dcc0f..4cbed7c080f 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 @@ -59,6 +59,29 @@ # 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/variables/gov/states/va/tax/income/va_agi_person.py b/policyengine_us/variables/gov/states/va/tax/income/va_agi_person.py index 6e28bae9175..68d78b1361b 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,29 +13,30 @@ class va_agi_person(Variable): def formula(person, period, parameters): person_fagi = person("adjusted_gross_income_person", period) - # Apply person-level age deduction directly (separate VAGI worksheet, Line 14). - age_deduction = person("va_age_deduction_person", period) - # Apply the Social Security / Tier 1 Railroad subtraction directly to the - # person who received the benefits (separate VAGI worksheet, Line 15). - # Virginia exempts federally taxable Social Security in full, so a spouse - # whose only income is Social Security has no separate Virginia taxable - # income. Prorating this subtraction by federal AGI share would instead - # shift it to a higher-income spouse and wrongly inflate this spouse's - # VAGI (affecting the Spouse Tax Adjustment). - social_security_subtraction = person("taxable_social_security", period) - total_social_security_subtraction = person.tax_unit( - "tax_unit_taxable_social_security", period + # Virginia's "Worksheet for Determining Separate Virginia Adjusted Gross + # Income" (Form 760 instructions) attributes each subtraction to the + # spouse who received the underlying income, then sets each spouse's + # separate VAGI. Apply the subtractions we can resolve at the person + # level directly to the recipient, and prorate only the remainder. + # + # Attributing these per person matters for the Spouse Tax Adjustment: a + # spouse whose only income is Virginia-exempt (Social Security, railroad + # retirement, or unemployment) has no separate Virginia taxable income. + # Prorating these subtractions by federal AGI share would instead shift + # them onto a higher-income spouse and wrongly inflate this spouse's VAGI. + age_deduction = person("va_age_deduction_person", period) # Line 14 + person_subtractions = ( + person("taxable_social_security", period) # Line 15 (Social Security) + + person("railroad_benefits", period) # Line 15 (Tier 1 Railroad) + + person("unemployment_compensation", period) # Line 17 ) - # Prorate the remaining (non-age, non-Social-Security) subtractions by - # federal AGI share. + directly_attributed = age_deduction + person_subtractions + total_directly_attributed = person.tax_unit.sum(directly_attributed) + # The remaining subtractions are only available at the tax-unit level + # (e.g. US government interest, the military and disability subtractions, + # the 529 deduction), so they are still prorated by federal AGI share. total_subtractions = person.tax_unit("va_subtractions", period) - total_age_deduction = person.tax_unit("va_age_deduction", period) - prorated_subtractions = max_( - total_subtractions - - total_age_deduction - - total_social_security_subtraction, - 0, - ) + prorated_subtractions = max_(total_subtractions - total_directly_attributed, 0) total_federal_agi = person.tax_unit.sum(person_fagi) prorate = where(total_federal_agi > 0, person_fagi / total_federal_agi, 0) person_prorated_subtractions = prorated_subtractions * prorate @@ -45,7 +46,6 @@ def formula(person, period, parameters): return ( person_fagi + person_additions - - age_deduction - - social_security_subtraction + - directly_attributed - person_prorated_subtractions ) From 8a0002e1038b8dfa235cf472c080d5cb7f7c9868 Mon Sep 17 00:00:00 2001 From: PavelMakarchuk Date: Wed, 24 Jun 2026 16:57:13 -0400 Subject: [PATCH 3/3] Attribute all Virginia subtractions per spouse via va_subtractions_person Completes the per-person attribution: rather than handling only Social Security, railroad, and unemployment, introduce va_subtractions_person which attributes every Virginia subtraction to the spouse who received the underlying income, mirroring the separate-VAGI worksheet (Lines 14-17). va_agi_person now subtracts this person-level total directly. Covered per person: age deduction, taxable Social Security, Tier 1 railroad, unemployment, US government interest, military basic pay, disability income, federal/state employee salary, and military benefit subtractions. The National Guard subtraction (capped on the household total) is allocated by each person's military service income, and the 529 deduction (a household contribution, not a person's income, so it cannot cause the bug) is prorated by federal AGI share. The per-person subtractions sum exactly to the tax-unit va_subtractions across all income types (verified), so tax-unit va_agi is unchanged. Adds a va_subtractions_person test and updates the legacy va_agi_person cases to the new per-person dependency. Full VA suite: 458 passed. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../fix-va-sta-social-security.fixed.md | 2 +- .../states/va/tax/income/va_agi_person.yaml | 8 +- .../va/tax/income/va_subtractions_person.yaml | 60 ++++++++++ .../gov/states/va/tax/income/va_agi_person.py | 41 ++----- .../va/tax/income/va_subtractions_person.py | 110 ++++++++++++++++++ 5 files changed, 186 insertions(+), 35 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/states/va/tax/income/va_subtractions_person.yaml create mode 100644 policyengine_us/variables/gov/states/va/tax/income/va_subtractions_person.py diff --git a/changelog.d/fix-va-sta-social-security.fixed.md b/changelog.d/fix-va-sta-social-security.fixed.md index 6f68d439415..72754de3a56 100644 --- a/changelog.d/fix-va-sta-social-security.fixed.md +++ b/changelog.d/fix-va-sta-social-security.fixed.md @@ -1 +1 @@ -Corrected the Virginia per-person adjusted gross income used by the Spouse Tax Adjustment to attribute exempt Social Security, railroad retirement, and unemployment subtractions to the recipient spouse, preventing the adjustment from being wrongly granted to couples where one spouse only has Virginia-exempt income. +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/va_agi_person.yaml b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/va_agi_person.yaml index 4cbed7c080f..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] 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 68d78b1361b..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 @@ -15,37 +15,16 @@ def formula(person, period, parameters): person_fagi = person("adjusted_gross_income_person", period) # Virginia's "Worksheet for Determining Separate Virginia Adjusted Gross # Income" (Form 760 instructions) attributes each subtraction to the - # spouse who received the underlying income, then sets each spouse's - # separate VAGI. Apply the subtractions we can resolve at the person - # level directly to the recipient, and prorate only the remainder. - # - # Attributing these per person matters for the Spouse Tax Adjustment: a - # spouse whose only income is Virginia-exempt (Social Security, railroad - # retirement, or unemployment) has no separate Virginia taxable income. - # Prorating these subtractions by federal AGI share would instead shift - # them onto a higher-income spouse and wrongly inflate this spouse's VAGI. - age_deduction = person("va_age_deduction_person", period) # Line 14 - person_subtractions = ( - person("taxable_social_security", period) # Line 15 (Social Security) - + person("railroad_benefits", period) # Line 15 (Tier 1 Railroad) - + person("unemployment_compensation", period) # Line 17 - ) - directly_attributed = age_deduction + person_subtractions - total_directly_attributed = person.tax_unit.sum(directly_attributed) - # The remaining subtractions are only available at the tax-unit level - # (e.g. US government interest, the military and disability subtractions, - # the 529 deduction), so they are still prorated by federal AGI share. - total_subtractions = person.tax_unit("va_subtractions", period) - prorated_subtractions = max_(total_subtractions - total_directly_attributed, 0) + # 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_prorated_subtractions = prorated_subtractions * prorate - # Prorate additions the same way - additions = person.tax_unit("va_additions", period) person_additions = additions * prorate - return ( - person_fagi - + person_additions - - directly_attributed - - person_prorated_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 + )