From 10775d89ef5081bbf6e9c6946156c9ff85d2e46e Mon Sep 17 00:00:00 2001 From: policyengine-bot Date: Fri, 12 Dec 2025 22:48:58 +0000 Subject: [PATCH 1/3] Fix VA spouse tax adjustment by calculating separate VAGI per person MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Virginia Spouse Tax Adjustment incorrectly calculated eligibility because va_agi_person prorated combined VA AGI by Federal AGI share, spreading deductions across both spouses instead of applying them to the correct person. Changes: - Created va_age_deduction_person to calculate age deduction per person - Created va_additions_person for person-level VA additions - Created va_subtractions_person for person-level VA subtractions - Fixed va_agi_person to calculate separate VAGI from person-level components instead of prorating combined VA AGI - Added integration test for elderly head with low income case This matches the VA Form 760 Spouse Tax Adjustment Worksheet (page 12) which shows separate Virginia AGI calculation for each spouse. Fixes #6958 🤖 Generated with Claude Code Co-Authored-By: Claude --- .../spouse_tax_adjustment/integration.yaml | 21 ++++++++++ .../income/additions/va_additions_person.py | 20 +++++++++ .../age_deduction/va_age_deduction_person.py | 42 +++++++++++++++++++ .../subtractions/va_subtractions_person.py | 27 ++++++++++++ .../gov/states/va/tax/income/va_agi_person.py | 22 ++++++---- 5 files changed, 125 insertions(+), 7 deletions(-) create mode 100644 policyengine_us/tests/policy/baseline/gov/states/va/tax/income/spouse_tax_adjustment/integration.yaml create mode 100644 policyengine_us/variables/gov/states/va/tax/income/additions/va_additions_person.py create mode 100644 policyengine_us/variables/gov/states/va/tax/income/subtractions/age_deduction/va_age_deduction_person.py create mode 100644 policyengine_us/variables/gov/states/va/tax/income/subtractions/va_subtractions_person.py diff --git a/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/spouse_tax_adjustment/integration.yaml b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/spouse_tax_adjustment/integration.yaml new file mode 100644 index 00000000000..3ea6ca9ab41 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/va/tax/income/spouse_tax_adjustment/integration.yaml @@ -0,0 +1,21 @@ +- name: VA Spouse Tax Adjustment - elderly head with low income + period: 2024 + absolute_error_margin: 1 + input: + people: + head: + age: 75 + employment_income: 11_752.73 + spouse: + age: 40 + employment_income: 55_728.48 + tax_units: + tax_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: VA + output: + va_spouse_tax_adjustment: 0 # Head's VAGI less exemptions <= 0, not eligible + va_income_tax: 1_402.22 # $1,802.22 - $400 rebate - $0 STA diff --git a/policyengine_us/variables/gov/states/va/tax/income/additions/va_additions_person.py b/policyengine_us/variables/gov/states/va/tax/income/additions/va_additions_person.py new file mode 100644 index 00000000000..761c26e6b06 --- /dev/null +++ b/policyengine_us/variables/gov/states/va/tax/income/additions/va_additions_person.py @@ -0,0 +1,20 @@ +from policyengine_us.model_api import * + + +class va_additions_person(Variable): + value_type = float + entity = Person + label = "Virginia additions to federal adjusted gross income for each person" + unit = USD + definition_period = YEAR + reference = ( + "https://law.lis.virginia.gov/vacodefull/title58.1/chapter3/article2/", + "https://www.tax.virginia.gov/sites/default/files/vatax-pdf/2022-760-instructions.pdf#page=24", + ) + defined_for = StateCode.VA + + def formula(person, period, parameters): + # Currently no person-specific additions are implemented + # When additions are added, they should be calculated here + # at the person level + return 0 diff --git a/policyengine_us/variables/gov/states/va/tax/income/subtractions/age_deduction/va_age_deduction_person.py b/policyengine_us/variables/gov/states/va/tax/income/subtractions/age_deduction/va_age_deduction_person.py new file mode 100644 index 00000000000..5fc178bd85c --- /dev/null +++ b/policyengine_us/variables/gov/states/va/tax/income/subtractions/age_deduction/va_age_deduction_person.py @@ -0,0 +1,42 @@ +from policyengine_us.model_api import * + + +class va_age_deduction_person(Variable): + value_type = float + entity = Person + label = "Virginia age deduction for each person" + unit = USD + definition_period = YEAR + reference = "https://www.tax.virginia.gov/sites/default/files/vatax-pdf/2022-760-instructions.pdf#page=16" + defined_for = StateCode.VA + + def formula(person, period, parameters): + p = parameters( + period + ).gov.states.va.tax.income.subtractions.age_deduction + + filing_status = person.tax_unit("filing_status", period) + + age = person("age", period) + birth_year = period.start.year - age + + agi = person("adjusted_gross_income_person", period) + + # Check if person is eligible for an age deduction + eligible = age >= p.age_minimum + + # Check if person is eligible for full deduction (no income limit) + eligible_for_full_deduction = ( + birth_year < p.birth_year_limit_for_full_amount + ) + + # Calculate the maximum allowable deduction amount per person + maximum_allowable_deduction = p.amount * eligible + + # Calculate the amount that the adjusted federal AGI exceeds the threshold + excess = max_(agi - p.threshold[filing_status], 0) + + # Reduce by the entire excess, unless eligible for the full deduction + reduction = excess * where(eligible_for_full_deduction, 0, 1) + + return max_(maximum_allowable_deduction - reduction, 0) diff --git a/policyengine_us/variables/gov/states/va/tax/income/subtractions/va_subtractions_person.py b/policyengine_us/variables/gov/states/va/tax/income/subtractions/va_subtractions_person.py new file mode 100644 index 00000000000..af37fc0b009 --- /dev/null +++ b/policyengine_us/variables/gov/states/va/tax/income/subtractions/va_subtractions_person.py @@ -0,0 +1,27 @@ +from policyengine_us.model_api import * + + +class va_subtractions_person(Variable): + value_type = float + entity = Person + label = "Virginia subtractions from federal adjusted gross income for each person" + unit = USD + definition_period = YEAR + reference = ( + "https://law.lis.virginia.gov/vacodefull/title58.1/chapter3/article2/", + "https://www.tax.virginia.gov/sites/default/files/vatax-pdf/2022-760-instructions.pdf#page=16", + ) + defined_for = StateCode.VA + + def formula(person, period, parameters): + # Calculate person-specific subtractions + # The main subtraction that varies by person is the age deduction + age_deduction = person("va_age_deduction_person", period) + + # Other subtractions (disability, federal employee, military, etc.) + # are calculated at the tax unit level by summing person-level amounts + # For the purpose of calculating separate VAGI per person, we focus on + # the age deduction which is the key subtraction that affects the + # spouse tax adjustment calculation + + return age_deduction 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 2b2d5bcf0a8..0b8f9d6da7e 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 @@ -12,11 +12,19 @@ class va_agi_person(Variable): defined_for = StateCode.VA def formula(person, period, parameters): - total_agi = person.tax_unit("va_agi", period) - person_agi = person("adjusted_gross_income_person", period) - total_federal_agi = person.tax_unit.sum(person_agi) + # Calculate separate Virginia AGI for each person + # This follows the VA Form 760 Spouse Tax Adjustment Worksheet + # which shows "Separate Virginia Adjusted Gross Income" calculation + # on page 12 of the instructions - prorate = np.zeros_like(total_agi) - mask = total_federal_agi > 0 - prorate[mask] = person_agi[mask] / total_federal_agi[mask] - return total_agi * prorate + # Start with federal AGI for this person + federal_agi = person("adjusted_gross_income_person", period) + + # Add Virginia-specific additions for this person + va_additions = person("va_additions_person", period) + + # Subtract Virginia-specific subtractions for this person + va_subtractions = person("va_subtractions_person", period) + + # Calculate separate VAGI for this person + return federal_agi + va_additions - va_subtractions From 2676fa4c6d9a439c1711038a5110fde4bd7da096 Mon Sep 17 00:00:00 2001 From: policyengine-bot Date: Fri, 12 Dec 2025 23:01:12 +0000 Subject: [PATCH 2/3] Add changelog and format code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with Claude Code Co-Authored-By: Claude --- changelog_entry.yaml | 4 ++++ .../gov/states/va/tax/income/additions/va_additions_person.py | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/changelog_entry.yaml b/changelog_entry.yaml index e69de29bb2d..223126755cd 100644 --- a/changelog_entry.yaml +++ b/changelog_entry.yaml @@ -0,0 +1,4 @@ +- bump: minor + changes: + fixed: + - Virginia spouse tax adjustment now correctly calculates eligibility by computing separate VAGI for each person instead of prorating combined VAGI diff --git a/policyengine_us/variables/gov/states/va/tax/income/additions/va_additions_person.py b/policyengine_us/variables/gov/states/va/tax/income/additions/va_additions_person.py index 761c26e6b06..a56b8f4e954 100644 --- a/policyengine_us/variables/gov/states/va/tax/income/additions/va_additions_person.py +++ b/policyengine_us/variables/gov/states/va/tax/income/additions/va_additions_person.py @@ -4,7 +4,9 @@ class va_additions_person(Variable): value_type = float entity = Person - label = "Virginia additions to federal adjusted gross income for each person" + label = ( + "Virginia additions to federal adjusted gross income for each person" + ) unit = USD definition_period = YEAR reference = ( From 68f522a89c8800d431ce0198d7a4c5c7961cd50a Mon Sep 17 00:00:00 2001 From: PavelMakarchuk Date: Sun, 14 Dec 2025 18:22:47 -0500 Subject: [PATCH 3/3] Fix va_agi_person tests for new separate VAGI calculation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update tests to reflect the new correct behavior where each person's VA AGI is calculated as their federal AGI +/- VA adjustments, rather than prorating the combined VA AGI. Also update uv.lock. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../states/va/tax/income/va_agi_person.yaml | 43 ++++++++++++++++--- uv.lock | 14 +++--- 2 files changed, 44 insertions(+), 13 deletions(-) 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 23d360813f4..cbe2a1434a2 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,37 +1,68 @@ -- name: Attributing the state adjusted gross income based on federal AGI +# VA AGI for each person is calculated as: +# Federal AGI + VA additions - VA subtractions (including age deduction) +# This is the "Separate Virginia Adjusted Gross Income" used in the +# Spouse Tax Adjustment Worksheet (Form 760 instructions, page 12) + +- name: Separate VAGI - two earners without VA subtractions period: 2021 input: people: person1: + age: 40 adjusted_gross_income_person: 10_000 person2: + age: 40 adjusted_gross_income_person: 30_000 tax_units: tax_unit: members: [person1, person2] - va_agi: 30_000 households: household: members: [person1, person2] state_code: VA output: - va_agi_person: [7_500, 22_500] + # Each person's VA AGI = their federal AGI (no additions/subtractions) + va_agi_person: [10_000, 30_000] -- name: One person without income +- name: Separate VAGI - one person without income period: 2021 input: people: person1: + age: 40 adjusted_gross_income_person: 10_000 person2: + age: 40 adjusted_gross_income_person: 0 tax_units: tax_unit: members: [person1, person2] - va_agi: 8_000 households: household: members: [person1, person2] state_code: VA output: - va_agi_person: [8_000, 0] + # Person 1 VA AGI = $10K, Person 2 VA AGI = $0 + va_agi_person: [10_000, 0] + +- name: Separate VAGI - elderly with age deduction + period: 2021 + input: + people: + person1: + age: 75 + adjusted_gross_income_person: 20_000 + person2: + age: 40 + adjusted_gross_income_person: 30_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: VA + output: + # Person 1: $20K - $12K age deduction = $8K + # Person 2: $30K (no age deduction) + va_agi_person: [8_000, 30_000] diff --git a/uv.lock b/uv.lock index dbcc6f6d620..87d3b248108 100644 --- a/uv.lock +++ b/uv.lock @@ -659,7 +659,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "1.2.2" +version = "1.2.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -673,9 +673,9 @@ dependencies = [ { name = "typer-slim" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/67/51/6db95c854e5eb3af8e0edfbfad7588983f63be39662054a49d5e116fb65d/huggingface_hub-1.2.2.tar.gz", hash = "sha256:b5b97bd37f4fe5b898a467373044649c94ee32006c032ce8fb835abe9d92ea28", size = 614598, upload-time = "2025-12-10T14:51:50.208Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/c8/9cd2fcb670ba0e708bfdf95a1177b34ca62de2d3821df0773bc30559af80/huggingface_hub-1.2.3.tar.gz", hash = "sha256:4ba57f17004fd27bb176a6b7107df579865d4cde015112db59184c51f5602ba7", size = 614605, upload-time = "2025-12-12T15:31:42.161Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/40/eb2f3a2c09bebf2fc989ba8bf701ce1f56b2f054b51e1a0fcb3e5d23f13a/huggingface_hub-1.2.2-py3-none-any.whl", hash = "sha256:0f55d7d22058fbf8b29d8095aeee80a7b695aa764f906a21e886c1f87223718f", size = 520964, upload-time = "2025-12-10T14:51:48.206Z" }, + { url = "https://files.pythonhosted.org/packages/df/8d/7ca723a884d55751b70479b8710f06a317296b1fa1c1dec01d0420d13e43/huggingface_hub-1.2.3-py3-none-any.whl", hash = "sha256:c9b7a91a9eedaa2149cdc12bdd8f5a11780e10de1f1024718becf9e41e5a4642", size = 520953, upload-time = "2025-12-12T15:31:40.339Z" }, ] [[package]] @@ -1519,7 +1519,7 @@ wheels = [ [[package]] name = "policyengine-us" -version = "1.458.1" +version = "1.459.2" source = { editable = "." } dependencies = [ { name = "microdf-python" }, @@ -2327,11 +2327,11 @@ wheels = [ [[package]] name = "tzdata" -version = "2025.2" +version = "2025.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] [[package]]