From 5afda4968d0cdf7b846e973d8ed0794064fd9d41 Mon Sep 17 00:00:00 2001 From: PavelMakarchuk Date: Wed, 7 Oct 2026 12:12:28 -0400 Subject: [PATCH 1/3] Offset SSTB and non-SSTB Schedule C results in federal AGI irs_gross_income counts each Schedule C category's profit on its own, but loss_ald took the loss only from the net total_self_employment_income, so a loss in one category next to a profit in the other never reached AGI. loss_ald now takes each category's profit and loss on its own. Fixes #9989. Co-Authored-By: Claude Opus 5.5 --- ...ix-schedule-c-category-losses-agi.fixed.md | 1 + .../self_employment_income.yaml | 29 +++++++++++++++++++ .../above_the_line_deductions/loss_ald.py | 8 +++-- 3 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 changelog.d/fix-schedule-c-category-losses-agi.fixed.md diff --git a/changelog.d/fix-schedule-c-category-losses-agi.fixed.md b/changelog.d/fix-schedule-c-category-losses-agi.fixed.md new file mode 100644 index 00000000000..feb576f30e0 --- /dev/null +++ b/changelog.d/fix-schedule-c-category-losses-agi.fixed.md @@ -0,0 +1 @@ +Deduct a Schedule C loss in one SSTB category from AGI when the same person has Schedule C profit in the other category. diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employment_income.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employment_income.yaml index 526b7ca01e0..54b7d53a1c0 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employment_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employment_income.yaml @@ -54,3 +54,32 @@ sstb_self_employment_income_behavioral_response: 1_000 self_employment_income: 0 sstb_self_employment_income: -9_000 + +- name: Non-SSTB Schedule C loss offsets SSTB Schedule C profit in AGI + # Two Schedules C: line 31 of (5,000) and 5,000 both go to Schedule 1 + # line 3, which is 0, so Form 1040 line 11 AGI is 0. + period: 2024 + input: + self_employment_income: -5_000 + sstb_self_employment_income: 5_000 + output: + adjusted_gross_income: 0 + +- name: SSTB Schedule C loss offsets non-SSTB Schedule C profit in AGI + period: 2024 + input: + self_employment_income: 5_000 + sstb_self_employment_income: -5_000 + output: + adjusted_gross_income: 0 + +- name: Offsetting Schedule C results leave AGI equal to wages + # Form 1040 line 1z 50,000 plus Schedule 1 line 3 of 0 gives line 11 AGI + # of 50,000. + period: 2024 + input: + employment_income: 50_000 + self_employment_income: -5_000 + sstb_self_employment_income: 5_000 + output: + adjusted_gross_income: 50_000 diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/loss_ald.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/loss_ald.py index d51b44af919..dd52e29d715 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/loss_ald.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/loss_ald.py @@ -31,8 +31,12 @@ def formula(tax_unit, period, parameters): # business amounts flow into gross income and losses are deducted here, # so allowable business losses are capped at business income plus # the threshold. - indiv_se_income = max_(0, person("total_self_employment_income", period)) - indiv_se_loss = max_(0, -person("total_self_employment_income", period)) + # irs_gross_income counts each Schedule C category's profit on its + # own, so each category's loss is deducted on its own here. + non_sstb_se = person("self_employment_income", period) + sstb_se = person("sstb_self_employment_income", period) + indiv_se_income = max_(0, non_sstb_se) + max_(0, sstb_se) + indiv_se_loss = max_(0, -non_sstb_se) + max_(0, -sstb_se) self_employment_income = tax_unit.sum(not_dependent * indiv_se_income) self_employment_loss = tax_unit.sum(not_dependent * indiv_se_loss) From 8bf92933c9745714ba646a78cab1b9237d06f685 Mon Sep 17 00:00:00 2001 From: PavelMakarchuk Date: Thu, 8 Oct 2026 10:05:23 -0400 Subject: [PATCH 2/3] Treat Schedule C categories separately in the loss invariant reference The all-member reference in test_dependent_losses_agi_invariants netted self_employment_income and sstb_self_employment_income per person before splitting income from loss, so it disagreed with loss_ald once loss_ald takes each category on its own (#9989). Use each input on its own, as irs_gross_income does. Co-Authored-By: Claude Opus 5.5 --- .../tests/core/test_dependent_losses_agi_invariants.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/policyengine_us/tests/core/test_dependent_losses_agi_invariants.py b/policyengine_us/tests/core/test_dependent_losses_agi_invariants.py index 432e36b895f..5ff3157bf0f 100644 --- a/policyengine_us/tests/core/test_dependent_losses_agi_invariants.py +++ b/policyengine_us/tests/core/test_dependent_losses_agi_invariants.py @@ -275,10 +275,9 @@ def test_units_without_dependents_match_all_member_formula(runs, units): """Differential check against the all-member formula.""" p = runs["irs"] filing_status = runs["with"]["filing_status"] - se = _by_person(units, "self_employment_income") + _by_person( - units, "sstb_self_employment_income" - ) - person_business = [se] + [_by_person(units, name) for name in BUSINESS_INPUTS[2:]] + # Each Schedule C category's profit and loss on its own, as + # irs_gross_income counts them (#9989). + person_business = [_by_person(units, name) for name in BUSINESS_INPUTS] income = sum(_unit_sum(runs, np.maximum(x, 0)) for x in person_business) loss = sum(_unit_sum(runs, np.maximum(-x, 0)) for x in person_business) limited_business_loss = np.minimum(loss, income + p.ald.loss.max[filing_status]) From d922bb77f4a5e8f7e2d73725b9b32a965feccbdd Mon Sep 17 00:00:00 2001 From: PavelMakarchuk Date: Mon, 12 Oct 2026 11:06:53 -0400 Subject: [PATCH 3/3] Treat Schedule C categories separately in state loss add-backs and allocations Wisconsin's homestead disqualified-loss add-back, Kentucky's spouse loss allocation and Maryland's two-income loss allocation took the Schedule C loss from the net total_self_employment_income, matching the old loss_ald. They now take each category on its own, as loss_ald does. The 2023 Schedule H instructions (line 11j, page 16) say not to net a business with a gain against one with a loss, and the 2025 Form 740 instructions (page 13, line 9) say each spouse claims their own income and deductions. The loss_ald extraction parity reference now treats each category as its own source as well. Co-Authored-By: Claude Opus 5.5 --- ...ix-schedule-c-category-losses-agi.fixed.md | 2 +- .../income/disqualified_loss_sources.yaml | 8 +++--- .../core/test_loss_ald_extraction_parity.py | 4 ++- .../states/ky/tax/income/ky_federal_agi.yaml | 27 +++++++++++++++++++ .../wi_homestead_disqualified_losses.yaml | 22 +++++++++++++++ .../wi/tax/income/wi_homestead_income.yaml | 4 +-- .../states/ky/tax/income/ky_federal_agi.py | 4 ++- .../subtractions/md_two_income_subtraction.py | 6 +++-- 8 files changed, 67 insertions(+), 10 deletions(-) diff --git a/changelog.d/fix-schedule-c-category-losses-agi.fixed.md b/changelog.d/fix-schedule-c-category-losses-agi.fixed.md index feb576f30e0..9a4de707704 100644 --- a/changelog.d/fix-schedule-c-category-losses-agi.fixed.md +++ b/changelog.d/fix-schedule-c-category-losses-agi.fixed.md @@ -1 +1 @@ -Deduct a Schedule C loss in one SSTB category from AGI when the same person has Schedule C profit in the other category. +Deduct a Schedule C loss in one SSTB category from AGI when the same person has Schedule C profit in the other category, and treat the two categories separately in the Wisconsin homestead loss add-back and the Kentucky and Maryland spouse loss allocations. diff --git a/policyengine_us/parameters/gov/states/wi/tax/income/credits/homestead/income/disqualified_loss_sources.yaml b/policyengine_us/parameters/gov/states/wi/tax/income/credits/homestead/income/disqualified_loss_sources.yaml index 7ab7c740d6b..a998975efd7 100644 --- a/policyengine_us/parameters/gov/states/wi/tax/income/credits/homestead/income/disqualified_loss_sources.yaml +++ b/policyengine_us/parameters/gov/states/wi/tax/income/credits/homestead/income/disqualified_loss_sources.yaml @@ -2,9 +2,11 @@ description: Wisconsin adds back the losses from these person-level income sourc values: 2021-01-01: # Schedule 4, line 2: net business loss from a sole proprietorship - # (Schedule C). SSTB and non-SSTB income are combined, as loss_ald - # deducts the loss of their total. - - total_self_employment_income + # (Schedule C). The line 11j instructions say to consider each business + # individually, so the SSTB and non-SSTB losses are added back + # separately, as loss_ald deducts them. + - self_employment_income + - sstb_self_employment_income - rental_income # line 4: net rental real estate and royalty loss # Form 4835 farm rental loss: rental real estate (71.52(1e)(d)) or net # farm loss (71.52(1e)(e)). diff --git a/policyengine_us/tests/core/test_loss_ald_extraction_parity.py b/policyengine_us/tests/core/test_loss_ald_extraction_parity.py index d7c2ee4ccfb..5b6701637cb 100644 --- a/policyengine_us/tests/core/test_loss_ald_extraction_parity.py +++ b/policyengine_us/tests/core/test_loss_ald_extraction_parity.py @@ -216,8 +216,10 @@ def _pre_extraction_reference(unit, threshold, capital_cap): for person, dependent in unit["members"]: if dependent: continue + # Each Schedule C category is its own source, as in irs_gross_income. business_sources = ( - person["self_employment_income"] + person["sstb_self_employment_income"], + person["self_employment_income"], + person["sstb_self_employment_income"], person["farm_operations_income"], person["rental_income"], person["farm_rent_income"], diff --git a/policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/ky_federal_agi.yaml b/policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/ky_federal_agi.yaml index 4d3e4e2e4a4..c6feabfc5df 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/ky_federal_agi.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/ky/tax/income/ky_federal_agi.yaml @@ -271,3 +271,30 @@ ky_agi: [50_000, -13_000] ky_modified_agi_if_joint: 37_000 ky_modified_agi_if_separate: 50_000 + +- name: A spouse's non-SSTB Schedule C loss offsets the same spouse's SSTB Schedule C profit + # 2025 Form 740 instructions, page 13, line 9: "Each spouse must claim his + # or her own income and deductions. The total of line 5, Columns A and B, + # must equal the combined federal adjusted gross income." The head's two + # Schedules C (line 31 of (5,000) and 5,000) net to 0 on Schedule 1 line 3, + # so the head's line 5 is 0 and the spouse's is the 40,000 of wages. + period: 2025 + input: + people: + head: + age: 45 + self_employment_income: -5_000 + sstb_self_employment_income: 5_000 + spouse: + age: 43 + employment_income: 40_000 + tax_units: + tax_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: KY + output: + adjusted_gross_income: 40_000 + ky_federal_agi: [0, 40_000] diff --git a/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_homestead_disqualified_losses.yaml b/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_homestead_disqualified_losses.yaml index 3b66f547433..e9dc4838dd7 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_homestead_disqualified_losses.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_homestead_disqualified_losses.yaml @@ -163,3 +163,25 @@ state_code: WI output: wi_homestead_disqualified_losses: 0 + +- name: Case 9, a non-SSTB sole proprietorship loss is not netted against SSTB sole proprietorship income. + period: 2023 + absolute_error_margin: 0.01 + input: + people: + head: + age: 70 + self_employment_income: -5_000 + sstb_self_employment_income: 5_000 + tax_units: + tax_unit: + members: [head] + households: + household: + members: [head] + state_code: WI + output: + # 2023 Schedule H instructions, line 11j (page 16): consider each business + # individually and do not net a business with a gain against one with a + # loss (Example 1). Schedule 4, line 2 = 5,000. + wi_homestead_disqualified_losses: 5_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_homestead_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_homestead_income.yaml index 2cea0b82040..744d7253c8a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_homestead_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/wi/tax/income/wi_homestead_income.yaml @@ -96,6 +96,6 @@ # Two Schedule C businesses (2023 instructions, page 16, Example 1): # Schedule 1 line 3 = 5,000 - 5,000 = 0, and Schedule 4 line 2 adds back # the 5,000 loss without netting. Household income = 0 + 5,000 = 5,000. - # loss_ald deducts the loss of total_self_employment_income (0 here), so - # the add-back follows that total and household income is the same. + # loss_ald deducts each category's loss on its own, so AGI is 0 and the + # add-back is the 5,000 non-SSTB loss. wi_homestead_income: 5_000 diff --git a/policyengine_us/variables/gov/states/ky/tax/income/ky_federal_agi.py b/policyengine_us/variables/gov/states/ky/tax/income/ky_federal_agi.py index 5286ea249d3..f8d17c331c6 100644 --- a/policyengine_us/variables/gov/states/ky/tax/income/ky_federal_agi.py +++ b/policyengine_us/variables/gov/states/ky/tax/income/ky_federal_agi.py @@ -45,8 +45,10 @@ def formula(person, period, parameters): business_loss = max_(loss - capital_loss, 0) not_dependent = ~person("is_tax_unit_dependent", period) owned_business_loss = 0 + # Each Schedule C category's loss counts on its own, as in loss_ald. for source in ( - "total_self_employment_income", + "self_employment_income", + "sstb_self_employment_income", "farm_operations_income", "rental_income", "farm_rent_income", diff --git a/policyengine_us/variables/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.py b/policyengine_us/variables/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.py index 8db3537a2d3..69e54011116 100644 --- a/policyengine_us/variables/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.py +++ b/policyengine_us/variables/gov/states/md/tax/income/agi/subtractions/md_two_income_subtraction.py @@ -1,9 +1,11 @@ from policyengine_us.model_api import * -# Person-level sources of the business losses in loss_ald. +# Person-level sources of the business losses in loss_ald, with each +# Schedule C category on its own, as loss_ald deducts them. BUSINESS_LOSS_SOURCES = [ - "total_self_employment_income", + "self_employment_income", + "sstb_self_employment_income", "farm_operations_income", "rental_income", "farm_rent_income",