From 3003f46b877bfd71a08fc9c478da149d943602f5 Mon Sep 17 00:00:00 2001 From: PavelMakarchuk Date: Tue, 6 Oct 2026 21:51:59 -0400 Subject: [PATCH 1/2] Keep capital gains and losses out of Massachusetts Part B income ma_part_b_gross_income subtracted Part A and Part C gross income from ma_gross_income. Those net capital losses the Schedule B and D way, but irs_gross_income adds each filer's net capital gain floored at zero, so the difference stayed in Part B: a long-term loss against a short-term gain reduced Part B, and one spouse's short-term loss against the other spouse's long-term gain added phantom Part B income. Subtract the same dividend and capital gain amounts irs_gross_income adds instead (M.G.L. c. 62 s. 2(b), s. 2(d)(1)(M)). Fixes #9953. Co-Authored-By: Claude Opus 5.5 --- changelog.d/ma-part-b-capital-loss.fixed.md | 1 + .../gross_income/ma_part_b_gross_income.yaml | 124 ++++++++++++++++++ .../gross_income/ma_part_b_gross_income.py | 20 ++- 3 files changed, 140 insertions(+), 5 deletions(-) create mode 100644 changelog.d/ma-part-b-capital-loss.fixed.md create mode 100644 policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/gross_income/ma_part_b_gross_income.yaml diff --git a/changelog.d/ma-part-b-capital-loss.fixed.md b/changelog.d/ma-part-b-capital-loss.fixed.md new file mode 100644 index 00000000000..432abc42013 --- /dev/null +++ b/changelog.d/ma-part-b-capital-loss.fixed.md @@ -0,0 +1 @@ +Keep capital gains and losses out of Massachusetts Part B income. diff --git a/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/gross_income/ma_part_b_gross_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/gross_income/ma_part_b_gross_income.yaml new file mode 100644 index 00000000000..050ab548a98 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/gross_income/ma_part_b_gross_income.yaml @@ -0,0 +1,124 @@ +# Capital gains and losses go on Schedules B and D, never on Form 1, +# lines 3 through 10 (Part B). Expected values are computed line by line +# from the 2023 Form 1, Schedule B and Schedule D. A single filer with +# 60,000 of wages and no other 5.0% income has Part B taxable income of +# 60,000 - 2,000 (Form 1, line 11a) - 4,400 (line 18) = 53,600 (line 19). + +- name: A long-term loss applied against short-term gains does not reduce Part B income + period: 2023 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 60_000 + non_qualified_dividend_income: 1_000 + short_term_capital_gains: 3_000 + long_term_capital_gains: -1_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: MA + output: + # Form 1, line 10 = line 3 = 60,000. + ma_part_b_gross_income: 60_000 + ma_part_b_taxable_income: 53_600 + # Schedule D, line 13 = -1,000; line 14 = Schedule B, line 25 + # = min(1,000, 3,000) = 1,000; line 15 = -1,000 + 1,000 = 0. + # Schedule B: line 26 = 3,000 - 1,000 = 2,000 = line 28; line 31 = 1,000; + # line 32 omitted (Schedule D, line 15 is 0); line 35 = 1,000 + 2,000. + ma_part_a_agi: 3_000 + # Schedule B, line 38 = line 9 = 1,000; line 39 = 3,000 - 1,000 = 2,000. + ma_part_a_taxable_dividend_income: 1_000 + ma_part_a_taxable_capital_gains_income: 2_000 + ma_part_c_agi: 0 + # 5% x (53,600 + 1,000) + 8.5% x 2,000 = 2,730 + 170. + ma_income_tax_before_credits: 2_900 + +- name: A long-term loss applied against short-term gains without dividends + period: 2023 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 60_000 + short_term_capital_gains: 3_000 + long_term_capital_gains: -1_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: MA + output: + ma_part_b_gross_income: 60_000 + # Schedule B, line 25 = 1,000; line 28 = line 39 = 2,000. + ma_part_a_agi: 2_000 + ma_part_a_taxable_capital_gains_income: 2_000 + # 5% x 53,600 + 8.5% x 2,000 = 2,680 + 170. + ma_income_tax_before_credits: 2_850 + +- name: A long-term loss larger than the short-term gain does not reduce Part B income + period: 2023 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 60_000 + non_qualified_dividend_income: 1_000 + short_term_capital_gains: 1_000 + long_term_capital_gains: -3_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: MA + output: + ma_part_b_gross_income: 60_000 + ma_part_b_taxable_income: 53_600 + # Schedule D, line 14 = Schedule B, line 25 = min(3,000, 1,000) = 1,000; + # line 15 = -2,000. Line 32 worksheet: line 2 = min(1,000, 2,000) = 1,000; + # line 4 = 1,000; line 5 = 2,000; line 6 = 1,000. Schedule B, + # line 33 = 1,000 - 1,000 = 0; line 35 = 0 + 0. Schedule D, line 17 + # = -2,000 + 1,000 = -1,000 is carried over (line 23). + ma_part_a_agi: 0 + ma_part_c_agi: 0 + +- name: One spouse's short-term loss against the other spouse's long-term gain does not add to Part B income + period: 2023 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 60_000 + long_term_capital_gains: 5_000 + spouse: + age: 40 + short_term_capital_gains: -2_000 + tax_units: + tax_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: MA + output: + # Form 1, line 10 = line 3 = 60,000; line 19 = 60,000 - 2,000 - 8,800. + ma_part_b_gross_income: 60_000 + ma_part_b_taxable_income: 49_200 + # Schedule B: line 19c = -2,000; line 20 = 0 (line 9 is 0); line 21 + # = -2,000; line 22 = Schedule D, line 14 = min(2,000, 5,000) = 2,000. + # Schedule D, line 15 = 5,000 - 2,000 = 3,000 = line 21. + ma_part_a_agi: 0 + ma_part_c_agi: 3_000 + # 5% x 49,200 + 5% x 3,000 = 2,460 + 150. + ma_income_tax_before_credits: 2_610 diff --git a/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_part_b_gross_income.py b/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_part_b_gross_income.py index 24aebb0a591..dca495afe44 100644 --- a/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_part_b_gross_income.py +++ b/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_part_b_gross_income.py @@ -11,10 +11,20 @@ class ma_part_b_gross_income(Variable): defined_for = StateCode.MA def formula(tax_unit, period, parameters): + # M.G.L. c. 62 § 2(b)(2): Part B gross income is Massachusetts gross + # income not included in Part A or Part C gross income. ma_gross_income = tax_unit("ma_gross_income", period) - ma_part_a_gross_income = tax_unit("ma_part_a_gross_income", period) - ma_part_c_gross_income = tax_unit("ma_part_c_gross_income", period) - return max_( - 0, - ma_gross_income - ma_part_a_gross_income - ma_part_c_gross_income, + # ma_gross_income starts from irs_gross_income, which adds each + # filer's dividends and net capital gain floored at zero. Remove those + # same amounts: they go on Schedules B and D, and capital losses never + # reduce Part B income (§ 2(d)(1)(M) disallows IRC § 62(a)(3)). + # Subtracting Part A and Part C gross income instead would leave the + # difference between their loss netting and irs_gross_income's in Part B. + person = tax_unit.members + is_dependent = person("is_tax_unit_dependent", period) + dividends = max_(0, person("dividend_income", period)) + capital_gains = max_(0, person("capital_gains", period)) + part_a_and_c_income = tax_unit.sum( + where(is_dependent, 0, dividends + capital_gains) ) + return max_(0, ma_gross_income - part_a_and_c_income) From 01b18738973aff7543073f78f2413f677f363db9 Mon Sep 17 00:00:00 2001 From: PavelMakarchuk Date: Tue, 6 Oct 2026 22:06:21 -0400 Subject: [PATCH 2/2] Count capital gain distributions on Massachusetts Schedule D non_sch_d_capital_gains (Form 1040 line 7 distributions without a federal Schedule D) entered ma_gross_income through irs_gross_income but neither Part A nor Part C, so it stayed in Part B. Massachusetts reports them on Schedule D, line 6: add them to the long-term amount in ma_part_c_gross_income and ma_part_a_agi, and take them out of ma_part_b_gross_income. Fixes #9957. Co-Authored-By: Claude Opus 5.5 --- changelog.d/ma-part-b-capital-loss.fixed.md | 2 +- .../gross_income/ma_part_b_gross_income.yaml | 104 ++++++++++++++++++ .../adjusted_gross_income/ma_part_a_agi.py | 6 +- .../gross_income/ma_part_b_gross_income.py | 5 +- .../gross_income/ma_part_c_gross_income.py | 7 +- 5 files changed, 119 insertions(+), 5 deletions(-) diff --git a/changelog.d/ma-part-b-capital-loss.fixed.md b/changelog.d/ma-part-b-capital-loss.fixed.md index 432abc42013..24904f8d490 100644 --- a/changelog.d/ma-part-b-capital-loss.fixed.md +++ b/changelog.d/ma-part-b-capital-loss.fixed.md @@ -1 +1 @@ -Keep capital gains and losses out of Massachusetts Part B income. +Keep capital gains and losses out of Massachusetts Part B income, and count capital gain distributions reported without a federal Schedule D on Massachusetts Schedule D (Part C). diff --git a/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/gross_income/ma_part_b_gross_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/gross_income/ma_part_b_gross_income.yaml index 050ab548a98..007a22c4a0f 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/gross_income/ma_part_b_gross_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/gross_income/ma_part_b_gross_income.yaml @@ -122,3 +122,107 @@ ma_part_c_agi: 3_000 # 5% x 49,200 + 5% x 3,000 = 2,460 + 150. ma_income_tax_before_credits: 2_610 + +- name: Capital gain distributions without a federal Schedule D are Part C income + period: 2023 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 60_000 + non_sch_d_capital_gains: 5_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: MA + output: + ma_part_b_gross_income: 60_000 + ma_part_b_taxable_income: 53_600 + # Schedule D, line 6 = 5,000 = lines 9, 11, 13, 15, 17, 19 and 21. + ma_part_c_gross_income: 5_000 + ma_part_c_agi: 5_000 + # 5% x 53,600 + 5% x 5,000 = 2,680 + 250. + ma_income_tax_before_credits: 2_930 + +- name: A short-term loss offsets capital gain distributions in Part C + period: 2023 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 60_000 + non_sch_d_capital_gains: 5_000 + short_term_capital_gains: -2_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: MA + output: + ma_part_b_gross_income: 60_000 + # Schedule B: line 19c = -2,000; line 20 = 0 (line 9 is 0); line 21 + # = -2,000; line 22 = Schedule D, line 14 = min(2,000, 5,000) = 2,000. + # Schedule D, line 15 = 5,000 - 2,000 = 3,000. + ma_part_c_agi: 3_000 + # 5% x 53,600 + 5% x 3,000 = 2,680 + 150. + ma_income_tax_before_credits: 2_830 + +- name: A long-term loss offsets capital gain distributions on Schedule D + period: 2023 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 60_000 + non_sch_d_capital_gains: 5_000 + long_term_capital_gains: -3_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: MA + output: + ma_part_b_gross_income: 60_000 + # Schedule D: line 1 = -3,000; line 6 = 5,000; line 9 = 2,000 = line 13 + # = line 15 = line 21. + ma_part_c_agi: 2_000 + # 5% x 53,600 + 5% x 2,000 = 2,680 + 100. + ma_income_tax_before_credits: 2_780 + +- name: Capital gain distributions that cover a long-term loss leave short-term gains whole + period: 2023 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 60_000 + non_sch_d_capital_gains: 5_000 + short_term_capital_gains: 3_000 + long_term_capital_gains: -1_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: MA + output: + ma_part_b_gross_income: 60_000 + # Schedule D, line 13 = -1,000 + 5,000 = 4,000, so Schedule B, line 25 + # is omitted: line 26 = line 24 = 3,000 = line 28 = line 39. + ma_part_a_agi: 3_000 + ma_part_a_taxable_capital_gains_income: 3_000 + ma_part_c_agi: 4_000 + # 5% x 53,600 + 8.5% x 3,000 + 5% x 4,000 = 2,680 + 255 + 200. + ma_income_tax_before_credits: 3_135 diff --git a/policyengine_us/variables/gov/states/ma/tax/income/adjusted_gross_income/ma_part_a_agi.py b/policyengine_us/variables/gov/states/ma/tax/income/adjusted_gross_income/ma_part_a_agi.py index 2120a051cec..cad995449ef 100644 --- a/policyengine_us/variables/gov/states/ma/tax/income/adjusted_gross_income/ma_part_a_agi.py +++ b/policyengine_us/variables/gov/states/ma/tax/income/adjusted_gross_income/ma_part_a_agi.py @@ -30,7 +30,11 @@ def formula(tax_unit, period, parameters): short_term_capital_loss, ), ) - long_term_capital_gains = add(tax_unit, period, ["long_term_capital_gains"]) + # Massachusetts Schedule D, line 13 includes capital gain + # distributions reported without a federal Schedule D (line 6). + long_term_capital_gains = add( + tax_unit, period, ["long_term_capital_gains", "non_sch_d_capital_gains"] + ) long_term_capital_loss = max_(0, -long_term_capital_gains) long_term_loss_against_short_term_gain = min_( diff --git a/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_part_b_gross_income.py b/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_part_b_gross_income.py index dca495afe44..302668b8391 100644 --- a/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_part_b_gross_income.py +++ b/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_part_b_gross_income.py @@ -24,7 +24,10 @@ def formula(tax_unit, period, parameters): is_dependent = person("is_tax_unit_dependent", period) dividends = max_(0, person("dividend_income", period)) capital_gains = max_(0, person("capital_gains", period)) + # Capital gain distributions reported without a federal Schedule D + # go on Massachusetts Schedule D, line 6. + distributions = max_(0, person("non_sch_d_capital_gains", period)) part_a_and_c_income = tax_unit.sum( - where(is_dependent, 0, dividends + capital_gains) + where(is_dependent, 0, dividends + capital_gains + distributions) ) return max_(0, ma_gross_income - part_a_and_c_income) diff --git a/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_part_c_gross_income.py b/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_part_c_gross_income.py index 9536895d297..cb800ca68d1 100644 --- a/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_part_c_gross_income.py +++ b/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_part_c_gross_income.py @@ -11,8 +11,11 @@ class ma_part_c_gross_income(Variable): defined_for = StateCode.MA def formula(tax_unit, period, parameters): - # Long-term capital gains - ltcg = add(tax_unit, period, ["long_term_capital_gains"]) + # Long-term capital gains, with capital gain distributions reported + # without a federal Schedule D (Massachusetts Schedule D, line 6) + ltcg = add( + tax_unit, period, ["long_term_capital_gains", "non_sch_d_capital_gains"] + ) # Short-term capital gains (can be negative) stcg = add(tax_unit, period, ["short_term_capital_gains"]) # Per MA Schedule B line 22, short-term losses offset long-term gains