diff --git a/changelog.d/liheap-shared-inputs.added.md b/changelog.d/liheap-shared-inputs.added.md new file mode 100644 index 00000000000..8c905719109 --- /dev/null +++ b/changelog.d/liheap-shared-inputs.added.md @@ -0,0 +1 @@ +Add a shared person-level self-employment expense input and derive gross self-employment income from existing net income and reported business expenses other than cost of goods sold. diff --git a/policyengine_us/tests/policy/baseline/household/income/person/self_employment/self_employment_gross_income.yaml b/policyengine_us/tests/policy/baseline/household/income/person/self_employment/self_employment_gross_income.yaml new file mode 100644 index 00000000000..19519a178ce --- /dev/null +++ b/policyengine_us/tests/policy/baseline/household/income/person/self_employment/self_employment_gross_income.yaml @@ -0,0 +1,175 @@ +- name: Case 1, reconstruct business income without including wages or changing net profit. + period: 2026 + input: + employment_income: 36_000 + self_employment_income: 18_000 + self_employment_expense: 6_000 + output: + self_employment_gross_income: 24_000 + self_employment_income: 18_000 + +- name: Case 2, business expenses cover regular, SSTB, and farm self-employment together. + period: 2026 + input: + self_employment_income: 6_000 + sstb_self_employment_income: 3_000 + farm_operations_income: 5_000 + self_employment_expense: 4_000 + output: + self_employment_gross_income: 18_000 + +- name: Case 3, preserve a business loss when adding back its expenses. + period: 2026 + input: + self_employment_income: -3_000 + self_employment_expense: 12_000 + output: + self_employment_gross_income: 9_000 + self_employment_income: -3_000 + +- name: Case 4, omitted expenses use the ordinary zero default. + period: 2026 + input: + self_employment_income: 12_000 + output: + self_employment_gross_income: 12_000 + self_employment_expense: 0 + +- name: Case 5, a loss without reported expenses cannot create negative gross income. + period: 2026 + input: + self_employment_income: -3_000 + output: + self_employment_gross_income: 0 + +- name: Case 6, a farm loss does not offset non-farm profit when expenses are omitted. + period: 2026 + input: + self_employment_income: 6_000 + farm_operations_income: -10_000 + output: + # Non-farm floor 6,000 + farm floor 0; the reconstruction is -4,000. + self_employment_gross_income: 6_000 + farm_operations_income: -10_000 + +- name: Case 7, employee work expenses do not belong in this business-cost input, which is added back unconditionally. + period: 2026 + input: + employment_income: 36_000 + self_employment_expense: 6_000 + output: + self_employment_gross_income: 6_000 + +- name: Case 8, cost of goods sold stays out of the add-back, so the result is gross income after it. + period: 2026 + input: + # Receipts 100,000 - cost of goods sold 60,000 = gross income 40,000. + # Gross income 40,000 - other expenses 25,000 = net profit 15,000. + self_employment_income: 15_000 + self_employment_expense: 25_000 + output: + self_employment_gross_income: 40_000 + +- name: Case 9, fully reported expenses reconstruct a farm loss alongside non-farm profit. + period: 2026 + input: + self_employment_income: 6_000 + farm_operations_income: -10_000 + self_employment_expense: 12_000 + output: + # 6,000 - 10,000 + 12,000 = 8,000, above the 6,000 per-source floor. + self_employment_gross_income: 8_000 + +- name: Case 10, partially reported expenses fall back to the per-source floor. + period: 2026 + input: + self_employment_income: 6_000 + farm_operations_income: -10_000 + self_employment_expense: 3_000 + output: + # The reconstruction is -1,000, below the 6,000 per-source floor. + self_employment_gross_income: 6_000 + +- name: Case 11, a regular business loss does not offset SSTB profit when expenses are omitted. + period: 2026 + input: + self_employment_income: -4_000 + sstb_self_employment_income: 9_000 + output: + # Regular floor 0 + SSTB floor 9,000; the reconstruction is 5,000. + self_employment_gross_income: 9_000 + +- name: Case 12, reconstruct gross from the existing net-income chain without changing wages or the SE tax base. + period: 2026 + absolute_error_margin: 0.01 + input: + employment_income: 36_000 + self_employment_income_before_lsr: 15_000 + self_employment_expense: 25_000 + output: + self_employment_gross_income: 40_000 + self_employment_income: 15_000 + employment_income: 36_000 + # The tax base still uses net profit: 15,000 * 0.9235. + taxable_self_employment_income: 13_852.50 + +- name: Case 13, directly supplied gross income does not infer net profit or expenses. + period: 2026 + input: + self_employment_gross_income: 40_000 + output: + self_employment_gross_income: 40_000 + self_employment_income: 0 + sstb_self_employment_income: 0 + farm_operations_income: 0 + self_employment_expense: 0 + taxable_self_employment_income: 0 + +- name: Case 14, partnership earnings remain separate from reconstructed sole-proprietor gross income. + period: 2026 + absolute_error_margin: 0.01 + input: + self_employment_income_before_lsr: 10_000 + partnership_self_employment_net_earnings: 5_000 + self_employment_expense: 3_000 + output: + self_employment_gross_income: 13_000 + partnership_self_employment_net_earnings: 5_000 + # Partnership SE earnings remain in the tax base, not in the add-back. + taxable_self_employment_income: 13_852.50 + +- name: Case 15, expense reconstruction and loss floors operate independently for each person. + period: 2026 + input: + people: + person1: + self_employment_income_before_lsr: 15_000 + self_employment_expense: 25_000 + person2: + sstb_self_employment_income_before_lsr: -3_000 + self_employment_expense: 12_000 + person3: + self_employment_income_before_lsr: 6_000 + farm_operations_income: -10_000 + self_employment_expense: 3_000 + person4: + employment_income: 36_000 + output: + self_employment_gross_income: [40_000, 9_000, 6_000, 0] + self_employment_income: [15_000, 0, 6_000, 0] + sstb_self_employment_income: [0, -3_000, 0, 0] + farm_operations_income: [0, 0, -10_000, 0] + employment_income: [0, 0, 0, 36_000] + +- name: Case 16, annual net income and expenses convert consistently for monthly consumers. + period: 2026-07 + input: + self_employment_income_before_lsr: + 2026: 15_000 + self_employment_expense: + 2026: 21_000 + output: + # Annual gross is 15,000 + 21,000 = 36,000; July receives one twelfth. + self_employment_gross_income: 3_000 + self_employment_income: 1_250 + self_employment_expense: 1_750 diff --git a/policyengine_us/variables/household/expense/person/self_employment_expense.py b/policyengine_us/variables/household/expense/person/self_employment_expense.py new file mode 100644 index 00000000000..aa118a52002 --- /dev/null +++ b/policyengine_us/variables/household/expense/person/self_employment_expense.py @@ -0,0 +1,22 @@ +from policyengine_us.model_api import * + + +class self_employment_expense(Variable): + value_type = float + entity = Person + definition_period = YEAR + unit = USD + label = "Self-employment business expenses" + documentation = ( + "Business expenses already deducted from regular, SSTB, and farm net " + "self-employment income, excluding cost of goods sold: tax year 2025 " + "Schedule C lines 28 and 30, and Schedule F line 33. Excludes employee " + "work expenses and personal income and self-employment taxes. Defaults " + "to zero when not reported. Used to reconstruct gross income, not to " + "reduce reported net income again or determine program-allowable costs." + ) + reference = ( + "https://www.irs.gov/pub/irs-prior/f1040sc--2025.pdf#page=1", + "https://www.irs.gov/pub/irs-prior/f1040sf--2025.pdf#page=1", + "https://www.irs.gov/pub/irs-prior/f1040sf--2025.pdf#page=2", + ) diff --git a/policyengine_us/variables/household/income/person/self_employment/self_employment_gross_income.py b/policyengine_us/variables/household/income/person/self_employment/self_employment_gross_income.py new file mode 100644 index 00000000000..904d2dbdd72 --- /dev/null +++ b/policyengine_us/variables/household/income/person/self_employment/self_employment_gross_income.py @@ -0,0 +1,41 @@ +from policyengine_us.model_api import * + + +class self_employment_gross_income(Variable): + value_type = float + entity = Person + definition_period = YEAR + unit = USD + label = "Gross self-employment income" + documentation = ( + "Gross income after cost of goods sold and before other business " + "expenses (tax year 2025 Schedule C line 7 and Schedule F line 9), " + "reconstructed from regular, SSTB, and farm net income plus " + "self_employment_expense. Excludes separately reported partnership " + "earnings. The result is floored at the sum of the three net sources " + "individually floored at zero; this modeling fallback can differ from " + "tax-reported gross income and is not an IRS rule. Non-farm net income " + "includes labor-supply responses." + ) + reference = ( + "https://www.irs.gov/pub/irs-prior/f1040sc--2025.pdf#page=1", + "https://www.irs.gov/pub/irs-prior/f1040sf--2025.pdf#page=1", + "https://www.irs.gov/pub/irs-prior/f1040sf--2025.pdf#page=2", + ) + + def formula(person, period, parameters): + net_income_sources = [ + "self_employment_income", + "sstb_self_employment_income", + "farm_operations_income", + ] + # Add back the expenses deducted from net profit, preserving losses + # until after the add-back. + net_income = add(person, period, net_income_sources) + expenses = person("self_employment_expense", period) + reconstructed = net_income + expenses + # Apply the modeling fallback separately to each net-income source. + source_floor = 0 + for source in net_income_sources: + source_floor = source_floor + max_(person(source, period), 0) + return max_(reconstructed, source_floor)