diff --git a/changelog.d/add-sstb-self-employment-income.added.md b/changelog.d/add-sstb-self-employment-income.added.md new file mode 100644 index 00000000000..0dd9f473abe --- /dev/null +++ b/changelog.d/add-sstb-self-employment-income.added.md @@ -0,0 +1 @@ +Add `sstb_self_employment_income` and split QBID into non-SSTB and SSTB components per IRC §199A(d). Mixed SSTB/non-SSTB wage-limited cases can also provide `sstb_w2_wages_from_qualified_business` and `sstb_unadjusted_basis_qualified_property` to match Form 8995-A's separate SSTB wage/UBIA inputs. diff --git a/policyengine_us/parameters/gov/household/market_income_sources.yaml b/policyengine_us/parameters/gov/household/market_income_sources.yaml index 8522d585507..6af5b1e8c92 100644 --- a/policyengine_us/parameters/gov/household/market_income_sources.yaml +++ b/policyengine_us/parameters/gov/household/market_income_sources.yaml @@ -3,6 +3,7 @@ values: 0000-01-01: - employment_income - self_employment_income + - sstb_self_employment_income - partnership_s_corp_income - gi_cash_assistance - farm_income diff --git a/policyengine_us/parameters/gov/irs/gross_income/sources.yaml b/policyengine_us/parameters/gov/irs/gross_income/sources.yaml index 1e3306d12cd..7901e9a5634 100644 --- a/policyengine_us/parameters/gov/irs/gross_income/sources.yaml +++ b/policyengine_us/parameters/gov/irs/gross_income/sources.yaml @@ -3,6 +3,7 @@ values: 2010-01-01: - irs_employment_income - self_employment_income + - sstb_self_employment_income - partnership_s_corp_income - farm_income - farm_rent_income diff --git a/policyengine_us/parameters/gov/states/mi/tax/income/household_resources.yaml b/policyengine_us/parameters/gov/states/mi/tax/income/household_resources.yaml index 39480a52589..1874281e480 100644 --- a/policyengine_us/parameters/gov/states/mi/tax/income/household_resources.yaml +++ b/policyengine_us/parameters/gov/states/mi/tax/income/household_resources.yaml @@ -6,7 +6,7 @@ values: - dividend_income - interest_income - farm_income - - self_employment_income + - total_self_employment_income - partnership_s_corp_income - rental_income - farm_rent_income diff --git a/policyengine_us/tests/core/test_behavioral_response_measurements.py b/policyengine_us/tests/core/test_behavioral_response_measurements.py index 23812f722d0..9eb7a0bb6f8 100644 --- a/policyengine_us/tests/core/test_behavioral_response_measurements.py +++ b/policyengine_us/tests/core/test_behavioral_response_measurements.py @@ -6,6 +6,8 @@ import policyengine_us.variables.gov.simulation.capital_gains_responses as capital_gains_module import policyengine_us.variables.gov.simulation.labor_supply_response.income_elasticity_lsr as income_lsr_module import policyengine_us.variables.gov.simulation.labor_supply_response.labor_supply_behavioral_response as labor_supply_module +import policyengine_us.variables.gov.simulation.labor_supply_response.self_employment_income_behavioral_response as self_employment_response_module +import policyengine_us.variables.gov.simulation.labor_supply_response.sstb_self_employment_income_behavioral_response as sstb_self_employment_response_module import policyengine_us.variables.gov.simulation.labor_supply_response.substitution_elasticity_lsr as substitution_lsr_module from policyengine_us.variables.gov.simulation.behavioral_response_measurements import ( BASELINE_BEHAVIORAL_RESPONSE_MEASUREMENT_BRANCH, @@ -118,6 +120,7 @@ def __init__(self, simulation): self.values = { "employment_income_before_lsr": np.array([50_000.0, 20_000.0]), "self_employment_income_before_lsr": np.array([0.0, 5_000.0]), + "sstb_self_employment_income_before_lsr": np.array([0.0, 0.0]), "long_term_capital_gains_before_response": np.array([10_000.0, 500.0]), } @@ -246,6 +249,7 @@ def test_lsr_effect_helpers_compute_from_measurements(): { "employment_income_before_lsr": np.array([50_000.0, -20_000.0]), "self_employment_income_before_lsr": np.array([10_000.0, 5_000.0]), + "sstb_self_employment_income_before_lsr": np.array([20_000.0, 0.0]), "income_elasticity": np.array([0.5, 1.0]), "substitution_elasticity": np.array([0.2, 0.4]), } @@ -263,14 +267,77 @@ def test_lsr_effect_helpers_compute_from_measurements(): wage_change_bound=0.8, ) - assert np.allclose(earnings_before_lsr(person, 2026), np.array([60_000.0, 0.0])) + assert np.allclose(earnings_before_lsr(person, 2026), np.array([80_000.0, 5_000.0])) assert np.allclose( calculate_income_lsr_effect(person, 2026, parameters, measurements), - np.array([3_000.0, 0.0]), + np.array([4_000.0, -2_500.0]), ) assert np.allclose( calculate_substitution_lsr_effect(person, 2026, parameters, measurements), - np.array([1_500.0, 0.0]), + np.array([2_000.0, 1_600.0]), + ) + + +def test_earnings_before_lsr_uses_sstb_loss_magnitude(): + person = FakePerson(simulation=SimpleNamespace()) + person.values.update( + { + "employment_income_before_lsr": np.array([30_000.0, 0.0]), + "self_employment_income_before_lsr": np.array([0.0, 0.0]), + "sstb_self_employment_income_before_lsr": np.array([-20_000.0, -10_000.0]), + } + ) + + assert np.allclose(earnings_before_lsr(person, 2026), np.array([50_000.0, 10_000.0])) + + +def test_behavioral_response_inputs_split_self_employment_between_buckets(): + person = FakePerson(simulation=SimpleNamespace()) + person.values.update( + { + "labor_supply_behavioral_response": np.array([1_000.0, 1_000.0]), + "employment_income_behavioral_response": np.array([0.0, 400.0]), + "self_employment_income_before_lsr": np.array([0.0, 10_000.0]), + "sstb_self_employment_income_before_lsr": np.array([20_000.0, 30_000.0]), + } + ) + + assert np.allclose( + self_employment_response_module.self_employment_income_behavioral_response.formula( + person, 2026, None + ), + np.array([0.0, 150.0]), + ) + assert np.allclose( + sstb_self_employment_response_module.sstb_self_employment_income_behavioral_response.formula( + person, 2026, None + ), + np.array([1_000.0, 450.0]), + ) + + +def test_behavioral_response_inputs_preserve_sstb_loss_bucket(): + person = FakePerson(simulation=SimpleNamespace()) + person.values.update( + { + "labor_supply_behavioral_response": np.array([1_000.0]), + "employment_income_behavioral_response": np.array([0.0]), + "self_employment_income_before_lsr": np.array([0.0]), + "sstb_self_employment_income_before_lsr": np.array([-10_000.0]), + } + ) + + assert np.allclose( + self_employment_response_module.self_employment_income_behavioral_response.formula( + person, 2026, None + ), + np.array([0.0]), + ) + assert np.allclose( + sstb_self_employment_response_module.sstb_self_employment_income_behavioral_response.formula( + person, 2026, None + ), + np.array([1_000.0]), ) 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 new file mode 100644 index 00000000000..44ce1f557fa --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employment_income.yaml @@ -0,0 +1,56 @@ +- name: Total self-employment income includes SSTB income + period: 2024 + input: + self_employment_income_before_lsr: 30_000 + sstb_self_employment_income: 20_000 + output: + total_self_employment_income: 50_000 + +- name: Self-employed health insurance ALD uses SSTB income + period: 2024 + input: + sstb_self_employment_income: 10_000 + self_employed_health_insurance_premiums: 12_000 + output: + self_employed_health_insurance_ald_person: 10_000 + +- name: Self-employed pension ALD uses SSTB income + period: 2024 + input: + sstb_self_employment_income: 8_000 + self_employed_pension_contributions: 9_000 + output: + self_employed_pension_contribution_ald_person: 8_000 + +- name: Loss ALD includes SSTB self-employment losses + period: 2024 + input: + sstb_self_employment_income: -20_000 + output: + loss_ald: 20_000 + +- name: Labor supply response preserves SSTB self-employment category + period: 2024 + input: + labor_supply_behavioral_response: 1_000 + employment_income_before_lsr: 0 + self_employment_income_before_lsr: 0 + sstb_self_employment_income_before_lsr: 10_000 + output: + self_employment_income_behavioral_response: 0 + sstb_self_employment_income_behavioral_response: 1_000 + self_employment_income: 0 + sstb_self_employment_income: 11_000 + +- name: Labor supply response preserves SSTB loss category + period: 2024 + input: + labor_supply_behavioral_response: 1_000 + employment_income_before_lsr: 0 + self_employment_income_before_lsr: 0 + sstb_self_employment_income_before_lsr: -10_000 + output: + self_employment_income_behavioral_response: 0 + sstb_self_employment_income_behavioral_response: 1_000 + self_employment_income: 0 + sstb_self_employment_income: -9_000 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/qbid/qbid_amount.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/qbid/qbid_amount.yaml index 5fc9d2ac348..3d83878de7e 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/qbid/qbid_amount.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/qbid/qbid_amount.yaml @@ -199,3 +199,102 @@ # REIT/PTP component = 0.20 * 25k = $5,000. # Total = $15,000. qbid_amount: 15_000 + +# Mixed SSTB / non-SSTB tests (Form 8995-A, columns A vs B) +- name: Mixed SSTB and non-SSTB above threshold - SSTB phased out, non-SSTB capped + period: 2024 + input: + qualified_business_income: 50_000 + sstb_qualified_business_income: 50_000 + w2_wages_from_qualified_business: 100_000 + unadjusted_basis_qualified_property: 0 + taxable_income_less_qbid: 300_000 + qualified_reit_and_ptp_income: 0 + business_is_sstb: false + filing_status: SINGLE + output: + # Single 2024 threshold $191,950, length $50,000. + # Reduction rate = min(1, (300k - 191.95k)/50k) = 1 → applicable rate = 0. + # Non-SSTB component: + # qbid_max = 0.20 * 50k = $10,000 + # wage_cap = 0.50 * 100k = $50,000 + # min($10k, $50k) = $10,000. + # SSTB component: 0 (applicable rate = 0). + # Total = $10,000. + qbid_amount: 10_000 + +- name: Mixed SSTB and non-SSTB below threshold - both get full 20% deduction + period: 2024 + input: + qualified_business_income: 50_000 + sstb_qualified_business_income: 50_000 + w2_wages_from_qualified_business: 0 + unadjusted_basis_qualified_property: 0 + taxable_income_less_qbid: 100_000 + qualified_reit_and_ptp_income: 0 + business_is_sstb: false + filing_status: SINGLE + output: + # Below threshold, so SSTB phaseout does not apply and W-2/UBIA limits do not bind. + # Non-SSTB: 0.20 * 50k = $10,000. + # SSTB: 0.20 * 50k = $10,000. + # Total = $20,000. + qbid_amount: 20_000 + +- name: Mixed SSTB and non-SSTB in phase-in range with separate allocable wages + absolute_error_margin: 0.01 + period: 2024 + input: + qualified_business_income: 100_000 + sstb_qualified_business_income: 100_000 + w2_wages_from_qualified_business: 20_000 + sstb_w2_wages_from_qualified_business: 10_000 + unadjusted_basis_qualified_property: 0 + sstb_unadjusted_basis_qualified_property: 0 + taxable_income_less_qbid: 200_000 + qualified_reit_and_ptp_income: 0 + business_is_sstb: false + filing_status: SINGLE + output: + # Single 2024 threshold $191,950, length $50,000. + # Reduction rate = (200k - 191.95k) / 50k = 0.161; applicable rate = 0.839. + # Non-SSTB W-2 wages = 10k => cap = 5k. + # Non-SSTB deduction = 20k - 0.161 * (20k - 5k) = 17,585.00. + # SSTB W-2 wages = 10k => applicable-percentage cap = 0.839 * 5k = 4,195. + # SSTB deduction = 16,780 - 0.161 * (16,780 - 4,195) = 14,753.82. + # Total = 32,338.82. + qbid_amount: 32_338.82 + +- name: Only SSTB QBI fully phased out above threshold + period: 2024 + input: + qualified_business_income: 0 + sstb_qualified_business_income: 100_000 + w2_wages_from_qualified_business: 0 + unadjusted_basis_qualified_property: 0 + taxable_income_less_qbid: 300_000 + qualified_reit_and_ptp_income: 0 + business_is_sstb: false + filing_status: SINGLE + output: + # Above threshold; SSTB applicable rate = 0; non-SSTB QBI = 0. + qbid_amount: 0 + +- name: Only SSTB QBI in phase-in range + period: 2024 + input: + qualified_business_income: 0 + sstb_qualified_business_income: 100_000 + w2_wages_from_qualified_business: 50_000 + unadjusted_basis_qualified_property: 0 + taxable_income_less_qbid: 216_950 + qualified_reit_and_ptp_income: 0 + business_is_sstb: false + filing_status: SINGLE + output: + # Reduction rate = (216_950 - 191_950) / 50_000 = 0.5 → applicable rate = 0.5 + # Reduced QBI = 50_000; reduced W-2 = 25_000. + # qbid_max (reduced) = 0.20 * 50_000 = 10_000. + # wage_cap (reduced) = 0.50 * 25_000 = 12_500. + # min(10_000, 12_500) = 10_000. + qbid_amount: 10_000 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/qbid/qualified_business_income.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/qbid/qualified_business_income.yaml index 6466d75f1b6..391f5e11004 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/qbid/qualified_business_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/qbid/qualified_business_income.yaml @@ -57,3 +57,37 @@ self_employed_pension_contribution_ald_person: 0 output: qualified_business_income: 9_000 + +- name: SSTB SE income excluded; deductions pro-rated to non-SSTB + period: 2026 + input: + self_employment_income: 30_000 + sstb_self_employment_income: 70_000 + self_employment_tax_ald_person: 1_000 + self_employed_health_insurance_ald_person: 0 + self_employed_pension_contribution_ald_person: 0 + output: + # Non-SSTB share = 30_000 / 100_000 = 0.3 + # Non-SSTB QBI = 30_000 - 0.3 * 1_000 = 29_700 + qualified_business_income: 29_700 + +- name: Only SSTB SE income, non-SSTB QBI is zero + period: 2026 + input: + sstb_self_employment_income: 100_000 + self_employment_tax_ald_person: 1_000 + self_employed_health_insurance_ald_person: 0 + self_employed_pension_contribution_ald_person: 0 + output: + qualified_business_income: 0 + +- name: Mixed-sign SSTB loss does not create a negative non-SSTB deduction share + period: 2026 + input: + self_employment_income: 100_000 + sstb_self_employment_income: -50_000 + self_employment_tax_ald_person: 1_000 + self_employed_health_insurance_ald_person: 0 + self_employed_pension_contribution_ald_person: 0 + output: + qualified_business_income: 99_000 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/qbid/qualified_business_income_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/qbid/qualified_business_income_deduction.yaml index 0ef16af721a..4feef0daf86 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/qbid/qualified_business_income_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/qbid/qualified_business_income_deduction.yaml @@ -57,3 +57,105 @@ qualified_business_income: 1_000 output: qualified_business_income_deduction: 2_000 + +- name: Deduction floor in effect with SSTB-only income + absolute_error_margin: 0.01 + period: 2026 + input: + people: + person1: + sstb_self_employment_income: 1_000 + self_employment_tax_ald_person: 0 + self_employed_health_insurance_ald_person: 0 + self_employed_pension_contribution_ald_person: 0 + qbid_amount: 200 + is_tax_unit_head: true + tax_units: + tax_unit: + members: [person1] + taxable_income_less_qbid: 600 + adjusted_net_capital_gain: 0 + families: + family: + members: [person1] + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_fips: 6 + output: + qualified_business_income_deduction: 400 + +# Integration test: doctor (SSTB) with rental property (non-SSTB) above threshold. +# Demonstrates the §199A(d)(3) phaseout reaching only the SSTB component +# while the non-SSTB business still receives the W-2 capped deduction. +- name: Mixed SSTB and non-SSTB above threshold + absolute_error_margin: 1 + period: 2025 + input: + people: + person1: + employment_income: 400_000 + self_employment_income: 50_000 + sstb_self_employment_income: 50_000 + w2_wages_from_qualified_business: 100_000 + # Zero out QBI deductions so the test isolates SSTB / non-SSTB routing + # rather than SE-tax / health-insurance / pension ALD pro-rating. + self_employment_tax_ald_person: 0 + self_employed_health_insurance_ald_person: 0 + self_employed_pension_contribution_ald_person: 0 + is_tax_unit_head: true + tax_units: + tax_unit: + members: [person1] + families: + family: + members: [person1] + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_fips: 6 + output: + # Single 2025 threshold $197,300, length $50,000. + # Taxable income is far above the phaseout, so applicable rate = 0. + # Non-SSTB QBI = $50,000; W-2 cap = 0.50 * $100,000 = $50,000. + # Non-SSTB component = min(20% * $50k, $50k) = $10,000. + # SSTB QBI = $50,000; SSTB component fully phased out = $0. + qualified_business_income_deduction: 10_000 + +- name: Pure SSTB below threshold gets full 20 percent deduction + absolute_error_margin: 1 + period: 2025 + input: + people: + person1: + employment_income: 50_000 + business_is_qualified: false + sstb_self_employment_income: 30_000 + # Zero out QBI deductions to isolate SSTB routing. + self_employment_tax_ald_person: 0 + self_employed_health_insurance_ald_person: 0 + self_employed_pension_contribution_ald_person: 0 + is_tax_unit_head: true + tax_units: + tax_unit: + members: [person1] + families: + family: + members: [person1] + spm_units: + spm_unit: + members: [person1] + households: + household: + members: [person1] + state_fips: 6 + output: + # Below the $197,300 single threshold so the SSTB phaseout does not apply. + # SSTB QBI component = 0.20 * $30,000 = $6,000. + qualified_business_income_deduction: 6_000 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/qbid/sstb_qualified_business_income.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/qbid/sstb_qualified_business_income.yaml new file mode 100644 index 00000000000..49a7c826630 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/deductions/qbid/sstb_qualified_business_income.yaml @@ -0,0 +1,65 @@ +- name: SSTB SE income only, no deductions + period: 2026 + input: + sstb_self_employment_income: 100_000 + self_employment_tax_ald_person: 0 + self_employed_health_insurance_ald_person: 0 + self_employed_pension_contribution_ald_person: 0 + output: + sstb_qualified_business_income: 100_000 + +- name: SSTB SE income with would_be_qualified flag false + period: 2026 + input: + sstb_self_employment_income: 100_000 + sstb_self_employment_income_would_be_qualified: false + self_employment_tax_ald_person: 0 + self_employed_health_insurance_ald_person: 0 + self_employed_pension_contribution_ald_person: 0 + output: + sstb_qualified_business_income: 0 + +- name: SSTB SE income still counts when business_is_qualified is false + period: 2026 + input: + business_is_qualified: false + sstb_self_employment_income: 100_000 + self_employment_tax_ald_person: 0 + self_employed_health_insurance_ald_person: 0 + self_employed_pension_contribution_ald_person: 0 + output: + sstb_qualified_business_income: 100_000 + +- name: Mixed SSTB and non-SSTB SE income, deductions pro-rated by gross share + period: 2026 + input: + self_employment_income: 30_000 + sstb_self_employment_income: 70_000 + self_employment_tax_ald_person: 1_000 + self_employed_health_insurance_ald_person: 0 + self_employed_pension_contribution_ald_person: 0 + output: + # SSTB share = 70_000 / 100_000 = 0.7 + # SSTB QBI = 70_000 - 0.7 * 1_000 = 69_300 + sstb_qualified_business_income: 69_300 + +- name: No SSTB SE income returns zero + period: 2026 + input: + self_employment_income: 50_000 + self_employment_tax_ald_person: 1_000 + self_employed_health_insurance_ald_person: 0 + self_employed_pension_contribution_ald_person: 0 + output: + sstb_qualified_business_income: 0 + +- name: Mixed-sign non-SSTB loss does not create a negative SSTB deduction share + period: 2026 + input: + self_employment_income: -50_000 + sstb_self_employment_income: 100_000 + self_employment_tax_ald_person: 1_000 + self_employed_health_insurance_ald_person: 0 + self_employed_pension_contribution_ald_person: 0 + output: + sstb_qualified_business_income: 99_000 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/self_employment/taxable_self_employment_income.yaml b/policyengine_us/tests/policy/baseline/gov/irs/self_employment/taxable_self_employment_income.yaml index 063afa46472..b1875a79a5e 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/self_employment/taxable_self_employment_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/self_employment/taxable_self_employment_income.yaml @@ -45,3 +45,13 @@ # Both Schedule C ($50k) and K-1 Box 14 ($30k) are subject to SE tax. # 80_000 * (1 - 0.5 * 0.153) = 80_000 * 0.9235 = 73_880 taxable_self_employment_income: 73_880 + +- name: SSTB self-employment income is subject to SE tax. + period: 2024 + input: + self_employment_income: 50_000 + sstb_self_employment_income: 30_000 + output: + # Both non-SSTB ($50k) and SSTB ($30k) Schedule C income are subject to SE tax. + # 80_000 * (1 - 0.5 * 0.153) = 80_000 * 0.9235 = 73_880 + taxable_self_employment_income: 73_880 diff --git a/policyengine_us/tests/policy/baseline/gov/local/ca/riv/general_relief/eligibility/ca_riv_general_relief_meets_work_requirements.yaml b/policyengine_us/tests/policy/baseline/gov/local/ca/riv/general_relief/eligibility/ca_riv_general_relief_meets_work_requirements.yaml index 1a6ff92b868..e84bff89368 100644 --- a/policyengine_us/tests/policy/baseline/gov/local/ca/riv/general_relief/eligibility/ca_riv_general_relief_meets_work_requirements.yaml +++ b/policyengine_us/tests/policy/baseline/gov/local/ca/riv/general_relief/eligibility/ca_riv_general_relief_meets_work_requirements.yaml @@ -73,3 +73,21 @@ members: [person1, person2, person3] output: ca_riv_general_relief_meets_work_requirements: [true, true, true] + +- name: Case 4, SSTB self-employment satisfies the work requirement. + period: 2025-01 + absolute_error_margin: 0.2 + input: + people: + person1: + age: 30 + sstb_self_employment_income: 25_000 + households: + household: + members: [person1] + in_riv: true + spm_units: + spm_unit: + members: [person1] + output: + ca_riv_general_relief_meets_work_requirements: true diff --git a/policyengine_us/tests/policy/baseline/gov/simulation/labor_supply_response/employment_income_behavioral_response.yaml b/policyengine_us/tests/policy/baseline/gov/simulation/labor_supply_response/employment_income_behavioral_response.yaml index 1ca2d8f572d..57a69c8fbf2 100644 --- a/policyengine_us/tests/policy/baseline/gov/simulation/labor_supply_response/employment_income_behavioral_response.yaml +++ b/policyengine_us/tests/policy/baseline/gov/simulation/labor_supply_response/employment_income_behavioral_response.yaml @@ -25,6 +25,16 @@ output: employment_income_behavioral_response: 800 # 1_200 * (40_000 / 60_000) +- name: Mixed income sources including SSTB + period: 2023 + input: + labor_supply_behavioral_response: 1_200 + employment_income_before_lsr: 40_000 + self_employment_income_before_lsr: 0 + sstb_self_employment_income_before_lsr: 20_000 + output: + employment_income_behavioral_response: 800 # 1_200 * (40_000 / 60_000) + - name: Negative self-employment income period: 2023 input: @@ -32,7 +42,7 @@ employment_income_before_lsr: 50_000 self_employment_income_before_lsr: -10_000 output: - employment_income_behavioral_response: 1_250 # max_(earnings, 0) = 40_000, emp_share = 50_000/40_000 = 1.25 + employment_income_behavioral_response: 833.3333333333334 # 1_000 * (50_000 / 60_000) - name: Zero total earnings period: 2023 @@ -50,4 +60,14 @@ employment_income_before_lsr: 30_000 self_employment_income_before_lsr: -40_000 # Net negative earnings output: - employment_income_behavioral_response: 1_000 # max_(earnings, 0) = 0, defaults emp_share = 1 + employment_income_behavioral_response: 428.57142857142856 # 1_000 * (30_000 / 70_000) + +- name: Negative SSTB self-employment income uses absolute magnitude + period: 2023 + input: + labor_supply_behavioral_response: 1_000 + employment_income_before_lsr: 30_000 + self_employment_income_before_lsr: 0 + sstb_self_employment_income_before_lsr: -20_000 + output: + employment_income_behavioral_response: 600 # 1_000 * (30_000 / 50_000) diff --git a/policyengine_us/tests/policy/baseline/gov/simulation/labor_supply_response/substitution_elasticity.yaml b/policyengine_us/tests/policy/baseline/gov/simulation/labor_supply_response/substitution_elasticity.yaml index 925bc4b7f42..fa1dc67969f 100644 --- a/policyengine_us/tests/policy/baseline/gov/simulation/labor_supply_response/substitution_elasticity.yaml +++ b/policyengine_us/tests/policy/baseline/gov/simulation/labor_supply_response/substitution_elasticity.yaml @@ -27,15 +27,15 @@ output: substitution_elasticity: 0 # TODO: Debug why single person isn't getting primary earner elasticity -- name: Negative total earnings should have zero elasticity +- name: Negative self-employment income uses earnings magnitude for decile period: 2023 input: employment_income_before_lsr: 50_000 self_employment_income_before_lsr: -60_000 # Net negative earnings gov.simulation.labor_supply_responses.elasticities.substitution.all: 0 - gov.simulation.labor_supply_responses.elasticities.substitution.by_position_and_decile.primary.1: 0.2 + gov.simulation.labor_supply_responses.elasticities.substitution.by_position_and_decile.primary.9: 0.2 output: - substitution_elasticity: 0 # max_(earnings, 0) = 0, so elasticity = 0 + substitution_elasticity: 0.2 - name: Positive net earnings after self-employment loss period: 2023 @@ -43,6 +43,28 @@ employment_income_before_lsr: 50_000 self_employment_income_before_lsr: -5_000 # Net positive earnings = 45_000 gov.simulation.labor_supply_responses.elasticities.substitution.all: 0 - gov.simulation.labor_supply_responses.elasticities.substitution.by_position_and_decile.primary.5: 0.15 # 45k falls in decile 5 + gov.simulation.labor_supply_responses.elasticities.substitution.by_position_and_decile.primary.6: 0.15 # 55k falls in decile 6 + output: + substitution_elasticity: 0.15 + +- name: Positive net earnings including SSTB income + period: 2023 + input: + employment_income_before_lsr: 45_000 + self_employment_income_before_lsr: 0 + sstb_self_employment_income_before_lsr: 5_000 + gov.simulation.labor_supply_responses.elasticities.substitution.all: 0 + gov.simulation.labor_supply_responses.elasticities.substitution.by_position_and_decile.primary.5: 0.15 + output: + substitution_elasticity: 0.15 + +- name: Negative SSTB self-employment income uses earnings magnitude for decile + period: 2023 + input: + employment_income_before_lsr: 30_000 + self_employment_income_before_lsr: 0 + sstb_self_employment_income_before_lsr: -20_000 + gov.simulation.labor_supply_responses.elasticities.substitution.all: 0 + gov.simulation.labor_supply_responses.elasticities.substitution.by_position_and_decile.primary.5: 0.15 output: substitution_elasticity: 0.15 diff --git a/policyengine_us/tests/policy/baseline/gov/states/dc/dhs/ccsp/eligibility/qualified_activity_or_need/dc_ccsp_qualified_activity_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/dc/dhs/ccsp/eligibility/qualified_activity_or_need/dc_ccsp_qualified_activity_eligible.yaml index 1fb414f0f3b..b86db4a16b3 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/dc/dhs/ccsp/eligibility/qualified_activity_or_need/dc_ccsp_qualified_activity_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/dc/dhs/ccsp/eligibility/qualified_activity_or_need/dc_ccsp_qualified_activity_eligible.yaml @@ -28,6 +28,16 @@ output: dc_ccsp_qualified_activity_eligible: true +- name: Case 3b, SSTB self-employment counts as working. + period: 2022 + input: + is_tax_unit_head_or_spouse: true + sstb_self_employment_income: 100 + is_full_time_student: false + state_code: DC + output: + dc_ccsp_qualified_activity_eligible: true + - name: Case 4, both parents are working, eligible. period: 2023-01 absolute_error_margin: 0.5 diff --git a/policyengine_us/tests/policy/baseline/gov/states/dc/tax/income/dc_self_employment_loss_addition.yaml b/policyengine_us/tests/policy/baseline/gov/states/dc/tax/income/dc_self_employment_loss_addition.yaml index 29ad13b8f3b..8d9f9f2f723 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/dc/tax/income/dc_self_employment_loss_addition.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/dc/tax/income/dc_self_employment_loss_addition.yaml @@ -69,6 +69,17 @@ # DC addition: max(0, 12_000 - 12_000) = 0 dc_self_employment_loss_addition: 0 +- name: SSTB self-employment loss contributes to DC add-back + absolute_error_margin: 0.01 + period: 2024 + input: + sstb_self_employment_income: -50_000 + state_code: DC + output: + # SSTB loss also flows through loss_ald and DC's self-employment loss add-back. + # DC addition: max(0, 50_000 - 12_000) = 38_000 + dc_self_employment_loss_addition: 38_000 + - name: dc_self_employment_loss_addition unit test 2 absolute_error_margin: 0.01 period: 2021 diff --git a/policyengine_us/tests/policy/baseline/gov/states/de/dss/poc/de_poc_activity_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/states/de/dss/poc/de_poc_activity_eligible.yaml index 53c1a5d310d..8d3c3a9bca9 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/de/dss/poc/de_poc_activity_eligible.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/de/dss/poc/de_poc_activity_eligible.yaml @@ -151,3 +151,22 @@ state_code: DE output: de_poc_activity_eligible: true + +- name: Case 5b, SSTB self-employed parent counts as activity eligible. + period: 2025-01 + input: + people: + person1: + age: 30 + sstb_self_employment_income: 18_000 + person2: + age: 4 + spm_units: + spm_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: DE + output: + de_poc_activity_eligible: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/il/dhs/ccap/il_ccap_parent_meets_working_requirements.yaml b/policyengine_us/tests/policy/baseline/gov/states/il/dhs/ccap/il_ccap_parent_meets_working_requirements.yaml index bff1725ba3b..db7f28cb0c1 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/il/dhs/ccap/il_ccap_parent_meets_working_requirements.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/il/dhs/ccap/il_ccap_parent_meets_working_requirements.yaml @@ -86,3 +86,32 @@ state_code: IL output: il_ccap_parent_meets_working_requirements: false + +- name: Case 4, SSTB self-employment counts as working. + period: 2023-01 + absolute_error_margin: 0.5 + input: + people: + person1: + sstb_self_employment_income: 9_600 + is_tax_unit_head_or_spouse: true + person2: + employment_income: 100 + is_tax_unit_head_or_spouse: true + person3: + age: 1 + employment_income: 0 + is_tax_unit_dependent: true + spm_units: + spm_unit: + members: [person1, person2, person3] + spm_unit_size: 3 + tax_units: + tax_unit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + state_code: IL + output: + il_ccap_parent_meets_working_requirements: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/ma_gross_income_loss_adjustment.yaml b/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/ma_gross_income_loss_adjustment.yaml new file mode 100644 index 00000000000..f217778eca9 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/ma/tax/income/ma_gross_income_loss_adjustment.yaml @@ -0,0 +1,7 @@ +- name: SSTB Schedule C losses reduce Massachusetts gross income + period: 2024 + input: + sstb_self_employment_income: -10_000 + state_code: MA + output: + ma_gross_income_loss_adjustment: -10_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/mi_household_resources.yaml b/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/mi_household_resources.yaml index 61ef47f9550..6aa31a2b2b4 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/mi_household_resources.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mi/tax/income/mi_household_resources.yaml @@ -45,6 +45,15 @@ # Total: 100,000 - 50,000 = 50,000 mi_household_resources: 50_000 +- name: Negative SSTB business income floored at 0 + period: 2024 + input: + sstb_self_employment_income: -50_000 + irs_employment_income: 100_000 + state_code: MI + output: + mi_household_resources: 50_000 + - name: Negative rental income floored at 0 period: 2024 input: diff --git a/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/deductions/mo_business_income_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/deductions/mo_business_income_deduction.yaml index 6c688f79937..53c05ec4d31 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/deductions/mo_business_income_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mo/tax/income/deductions/mo_business_income_deduction.yaml @@ -30,3 +30,14 @@ state_code: MO output: mo_business_income_deduction: 0 + +- name: 2023 SSTB-only qualified business income + period: 2023 + input: + sstb_self_employment_income: 10_000 + self_employment_tax_ald_person: 0 + self_employed_health_insurance_ald_person: 0 + self_employed_pension_contribution_ald_person: 0 + state_code: MO + output: + mo_business_income_deduction: 2_000 diff --git a/policyengine_us/tests/policy/baseline/gov/states/nd/tax/income/nd_mpc.yaml b/policyengine_us/tests/policy/baseline/gov/states/nd/tax/income/nd_mpc.yaml index d9763404b10..bcbd6cf2fcd 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/nd/tax/income/nd_mpc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/nd/tax/income/nd_mpc.yaml @@ -104,3 +104,29 @@ state_code: ND output: nd_mpc: 286.64 + +- name: Test 5, SSTB self-employment income counts toward qualified income + absolute_error_margin: 0.01 + period: 2023 + input: + people: + person1: + is_tax_unit_head: true + age: 41 + sstb_self_employment_income: 200_000 + person2: + is_tax_unit_spouse: true + age: 41 + irs_employment_income: 180_000 + spm_units: + spm_unit: + members: [person1, person2] + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_code: ND + output: + nd_mpc: 287 diff --git a/policyengine_us/tests/policy/baseline/gov/states/vt/dcf/ccfap/vt_ccfap_meets_activity_test.yaml b/policyengine_us/tests/policy/baseline/gov/states/vt/dcf/ccfap/vt_ccfap_meets_activity_test.yaml new file mode 100644 index 00000000000..e25cacd8d6a --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/vt/dcf/ccfap/vt_ccfap_meets_activity_test.yaml @@ -0,0 +1,19 @@ +- name: SSTB self-employment satisfies the Vermont activity test + period: 2025-01 + input: + people: + parent: + age: 30 + sstb_self_employment_income: 30_000 + child: + age: 4 + is_tax_unit_dependent: true + spm_units: + spm_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: VT + output: + vt_ccfap_meets_activity_test: true diff --git a/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/vt_child_care_contributions.yaml b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/vt_child_care_contributions.yaml index 9db64db2077..4a6a4a955e5 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/vt_child_care_contributions.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/vt_child_care_contributions.yaml @@ -39,3 +39,11 @@ state_code: VT output: vt_child_care_contributions: 0 + +- name: SSTB self-employment income is counted toward the child care contributions + period: 2025 + input: + sstb_self_employment_income: 10_000 + state_code: VT + output: + vt_child_care_contributions: 11 diff --git a/policyengine_us/tests/policy/baseline/gov/usda/snap/income/deductions/self_employment_income/snap_self_employment_expense_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/usda/snap/income/deductions/self_employment_income/snap_self_employment_expense_deduction.yaml index 1b9717b840a..9782da87bee 100644 --- a/policyengine_us/tests/policy/baseline/gov/usda/snap/income/deductions/self_employment_income/snap_self_employment_expense_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/usda/snap/income/deductions/self_employment_income/snap_self_employment_expense_deduction.yaml @@ -53,3 +53,13 @@ output: # 40% of $1,000 = $400, but actual expenses ($600) are greater snap_self_employment_expense_deduction: 600 + +- name: Alaska simplified deduction uses SSTB income before labor supply response + period: 2025 + input: + sstb_self_employment_income_before_lsr: 300 + sstb_self_employment_income_behavioral_response: 200 + snap_self_employment_income_expense: 100 + state_code: AK + output: + snap_self_employment_expense_deduction: 150 diff --git a/policyengine_us/tests/policy/baseline/gov/usda/snap/income/deductions/self_employment_income/snap_self_employment_income_after_expense_deduction.yaml b/policyengine_us/tests/policy/baseline/gov/usda/snap/income/deductions/self_employment_income/snap_self_employment_income_after_expense_deduction.yaml index 27b0cf4543b..4c412560920 100644 --- a/policyengine_us/tests/policy/baseline/gov/usda/snap/income/deductions/self_employment_income/snap_self_employment_income_after_expense_deduction.yaml +++ b/policyengine_us/tests/policy/baseline/gov/usda/snap/income/deductions/self_employment_income/snap_self_employment_income_after_expense_deduction.yaml @@ -5,3 +5,12 @@ snap_self_employment_expense_deduction: 300 output: snap_self_employment_income_after_expense_deduction: 200 + +- name: SSTB income uses the pre-response amount after SNAP deduction + period: 2022 + input: + sstb_self_employment_income_before_lsr: 500 + sstb_self_employment_income_behavioral_response: 200 + snap_self_employment_expense_deduction: 300 + output: + snap_self_employment_income_after_expense_deduction: 200 diff --git a/policyengine_us/tests/policy/baseline/household/income/household/household_market_income.yaml b/policyengine_us/tests/policy/baseline/household/income/household/household_market_income.yaml index 1c5a2627ccc..546ce8ffb3a 100644 --- a/policyengine_us/tests/policy/baseline/household/income/household/household_market_income.yaml +++ b/policyengine_us/tests/policy/baseline/household/income/household/household_market_income.yaml @@ -34,6 +34,18 @@ output: household_market_income: 50_000 +- name: SSTB self-employment income is market income + period: 2026 + input: + people: + person: + sstb_self_employment_income: 50_000 + households: + household: + members: [person] + output: + household_market_income: 50_000 + - name: Employment income plus UC does not double count period: 2026 input: diff --git a/policyengine_us/tests/policy/baseline/household/income/person/weekly_hours_worked.yaml b/policyengine_us/tests/policy/baseline/household/income/person/weekly_hours_worked.yaml index 8c732e8e1f7..8b414421d59 100644 --- a/policyengine_us/tests/policy/baseline/household/income/person/weekly_hours_worked.yaml +++ b/policyengine_us/tests/policy/baseline/household/income/person/weekly_hours_worked.yaml @@ -57,3 +57,33 @@ weekly_hours_worked_behavioural_response_income_elasticity: 0 weekly_hours_worked_behavioural_response_substitution_elasticity: 0 weekly_hours_worked_behavioural_response: 0 + +- name: Weekly hours worked includes SSTB self-employment earnings + period: 2022 + input: + weekly_hours_worked_before_lsr: 40 + labor_supply_behavioral_response: 1 + income_elasticity_lsr: 0.1 + substitution_elasticity_lsr: 0.2 + employment_income_before_lsr: 100_000 + self_employment_income_before_lsr: 0 + sstb_self_employment_income_before_lsr: 50_000 + output: + weekly_hours_worked_behavioural_response_income_elasticity: 2.6666667e-05 + weekly_hours_worked_behavioural_response_substitution_elasticity: 5.3333333e-05 + weekly_hours_worked_behavioural_response: 8.0000000e-05 + +- name: Weekly hours worked uses SSTB loss magnitude + period: 2022 + input: + weekly_hours_worked_before_lsr: 40 + labor_supply_behavioral_response: 1 + income_elasticity_lsr: 0.1 + substitution_elasticity_lsr: 0.2 + employment_income_before_lsr: 100_000 + self_employment_income_before_lsr: 0 + sstb_self_employment_income_before_lsr: -50_000 + output: + weekly_hours_worked_behavioural_response_income_elasticity: 2.6666667e-05 + weekly_hours_worked_behavioural_response_substitution_elasticity: 5.3333333e-05 + weekly_hours_worked_behavioural_response: 8.0000000e-05 diff --git a/policyengine_us/tests/policy/contrib/taxsim/outputs/taxsim_outputs.yaml b/policyengine_us/tests/policy/contrib/taxsim/outputs/taxsim_outputs.yaml index 41ae60e5387..e2bac81c6c6 100644 --- a/policyengine_us/tests/policy/contrib/taxsim/outputs/taxsim_outputs.yaml +++ b/policyengine_us/tests/policy/contrib/taxsim/outputs/taxsim_outputs.yaml @@ -168,6 +168,26 @@ taxsim_psemp: 75000 taxsim_ssemp: 25000 +- name: SSTB self-employment income - TAXSIM outputs + period: 2024 + input: + people: + head: + age: 45 + sstb_self_employment_income: 75000 + is_tax_unit_head: true + spouse: + age: 43 + sstb_self_employment_income: 25000 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + filing_status: JOINT + output: + taxsim_psemp: 75000 + taxsim_ssemp: 25000 + - name: Unemployment income - TAXSIM outputs period: 2024 input: @@ -209,6 +229,32 @@ taxsim_sbusinc: 50000 taxsim_scorp: 150000 +- name: SSTB self-employment income contributes to TAXSIM QBI outputs + period: 2024 + input: + people: + head: + age: 50 + sstb_self_employment_income: 100000 + self_employment_tax_ald_person: 0 + self_employed_health_insurance_ald_person: 0 + self_employed_pension_contribution_ald_person: 0 + is_tax_unit_head: true + spouse: + age: 48 + sstb_self_employment_income: 50000 + self_employment_tax_ald_person: 0 + self_employed_health_insurance_ald_person: 0 + self_employed_pension_contribution_ald_person: 0 + is_tax_unit_spouse: true + tax_units: + tax_unit: + members: [head, spouse] + filing_status: JOINT + output: + taxsim_pbusinc: 100000 + taxsim_sbusinc: 50000 + - name: State code NY - TAXSIM outputs period: 2024 input: diff --git a/policyengine_us/tests/variables/household/emp_self_emp_ratio.yaml b/policyengine_us/tests/variables/household/emp_self_emp_ratio.yaml index 3def30e52e5..4d36f929969 100644 --- a/policyengine_us/tests/variables/household/emp_self_emp_ratio.yaml +++ b/policyengine_us/tests/variables/household/emp_self_emp_ratio.yaml @@ -29,3 +29,12 @@ self_employment_income: 0 output: emp_self_emp_ratio: 1.0 + +- name: SSTB self-employment income is included in the earnings ratio + period: 2024 + input: + employment_income: 25_000 + self_employment_income: 0 + sstb_self_employment_income: 25_000 + output: + emp_self_emp_ratio: 0.5 diff --git a/policyengine_us/variables/contrib/taxsim/taxsim_pbusinc.py b/policyengine_us/variables/contrib/taxsim/taxsim_pbusinc.py index 09b9a544153..5771c904489 100644 --- a/policyengine_us/variables/contrib/taxsim/taxsim_pbusinc.py +++ b/policyengine_us/variables/contrib/taxsim/taxsim_pbusinc.py @@ -10,6 +10,8 @@ class taxsim_pbusinc(Variable): def formula(tax_unit, period, parameters): person = tax_unit.members - qbi = person("qualified_business_income", period) + qbi = person("qualified_business_income", period) + person( + "sstb_qualified_business_income", period + ) is_head = person("is_tax_unit_head", period) return tax_unit.sum(qbi * is_head) diff --git a/policyengine_us/variables/contrib/taxsim/taxsim_psemp.py b/policyengine_us/variables/contrib/taxsim/taxsim_psemp.py index 6b1980b78cd..a4001737486 100644 --- a/policyengine_us/variables/contrib/taxsim/taxsim_psemp.py +++ b/policyengine_us/variables/contrib/taxsim/taxsim_psemp.py @@ -11,5 +11,5 @@ class taxsim_psemp(Variable): def formula(tax_unit, period, parameters): person = tax_unit.members is_primary = person("is_tax_unit_head", period) - semp = person("self_employment_income", period) + semp = person("total_self_employment_income", period) return tax_unit.sum(semp * is_primary) diff --git a/policyengine_us/variables/contrib/taxsim/taxsim_sbusinc.py b/policyengine_us/variables/contrib/taxsim/taxsim_sbusinc.py index 5e5cb7713c3..c7df67eeaf4 100644 --- a/policyengine_us/variables/contrib/taxsim/taxsim_sbusinc.py +++ b/policyengine_us/variables/contrib/taxsim/taxsim_sbusinc.py @@ -10,6 +10,8 @@ class taxsim_sbusinc(Variable): def formula(tax_unit, period, parameters): person = tax_unit.members - qbi = person("qualified_business_income", period) + qbi = person("qualified_business_income", period) + person( + "sstb_qualified_business_income", period + ) is_spouse = person("is_tax_unit_spouse", period) return tax_unit.sum(qbi * is_spouse) diff --git a/policyengine_us/variables/contrib/taxsim/taxsim_ssemp.py b/policyengine_us/variables/contrib/taxsim/taxsim_ssemp.py index c7ff4ab36a8..cb45a8af50b 100644 --- a/policyengine_us/variables/contrib/taxsim/taxsim_ssemp.py +++ b/policyengine_us/variables/contrib/taxsim/taxsim_ssemp.py @@ -11,5 +11,5 @@ class taxsim_ssemp(Variable): def formula(tax_unit, period, parameters): person = tax_unit.members is_primary = person("is_tax_unit_spouse", period) - semp = person("self_employment_income", period) + semp = person("total_self_employment_income", period) return tax_unit.sum(semp * is_primary) 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 531880bfa4a..d71816053e8 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 @@ -14,7 +14,7 @@ def formula(tax_unit, period, parameters): filing_status = tax_unit("filing_status", period) max_loss = parameters(period).gov.irs.ald.loss.max[filing_status] person = tax_unit.members - indiv_se_loss = max_(0, -person("self_employment_income", period)) + indiv_se_loss = max_(0, -person("total_self_employment_income", period)) self_employment_loss = tax_unit.sum(indiv_se_loss) limited_capital_loss = tax_unit("limited_capital_loss", period) return min_(max_loss, self_employment_loss + limited_capital_loss) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald_person.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald_person.py index 2d7d333ae23..8ecda97461a 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald_person.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_health_insurance_ald_person.py @@ -11,6 +11,6 @@ class self_employed_health_insurance_ald_person(Variable): reference = "https://www.law.cornell.edu/uscode/text/26/162#l" def formula(person, period, parameters): - earnings = max_(0, person("self_employment_income", period)) + earnings = max_(0, person("total_self_employment_income", period)) premiums = person("self_employed_health_insurance_premiums", period) return min_(earnings, premiums) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_pension_contribution_ald_person.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_pension_contribution_ald_person.py index 8b630cf6250..e027dd1a7c2 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_pension_contribution_ald_person.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/self_employed_pension_contribution_ald_person.py @@ -11,6 +11,6 @@ class self_employed_pension_contribution_ald_person(Variable): reference = "https://www.law.cornell.edu/uscode/text/26/162#l" def formula(person, period, parameters): - earnings = max_(0, person("self_employment_income", period)) + earnings = max_(0, person("total_self_employment_income", period)) contributions = person("self_employed_pension_contributions", period) return min_(earnings, contributions) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/earned_income/earned_income.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/earned_income/earned_income.py index 73dd3522683..156ce3093fd 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/earned_income/earned_income.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/irs_gross_income/earned_income/earned_income.py @@ -9,4 +9,8 @@ class earned_income(Variable): documentation = "Income from wages or self-employment" definition_period = YEAR - adds = ["employment_income", "self_employment_income"] + adds = [ + "employment_income", + "self_employment_income", + "sstb_self_employment_income", + ] diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/qualified_business_income_deduction/qbid_amount.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/qualified_business_income_deduction/qbid_amount.py index a8c0800a33a..262782294b2 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/qualified_business_income_deduction/qbid_amount.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/qualified_business_income_deduction/qbid_amount.py @@ -9,58 +9,122 @@ class qbid_amount(Variable): definition_period = YEAR reference = ( "https://www.law.cornell.edu/uscode/text/26/199A#b_1", + "https://www.law.cornell.edu/uscode/text/26/199A#d_3", "https://www.irs.gov/pub/irs-prior/p535--2018.pdf", "https://www.irs.gov/pub/irs-pdf/f8995.pdf", + "https://www.irs.gov/pub/irs-pdf/f8995a.pdf", ) def formula(person, period, parameters): - # computations follow logic in 2018 IRS Publication 535, - # Worksheet 12-A (and Schedule A for SSTB) + # Computations follow logic in 2018 IRS Publication 535, + # Worksheet 12-A (and Schedule A for SSTB). The non-SSTB and SSTB + # categories are computed separately so the §199A(d)(3) applicable- + # percentage phaseout reduces only the SSTB component above the + # threshold (Form 8995-A, Part II, columns A vs. B). p = parameters(period).gov.irs.deductions.qbi - # compute maximum QBID amount - qbi = person("qualified_business_income", period) - qbid_max = p.max.rate * qbi # Worksheet 12-A, line 3 - # compute caps - w2_wages = person("w2_wages_from_qualified_business", period) - b_property = person("unadjusted_basis_qualified_property", period) - wage_cap = w2_wages * p.max.w2_wages.rate # Worksheet 12-A, line 5 - alt_cap = ( # Worksheet 12-A, line 9 - w2_wages * p.max.w2_wages.alt_rate - + b_property * p.max.business_property.rate - ) - full_cap = max_(wage_cap, alt_cap) # Worksheet 12-A, line 10 - # compute phase-out ranges + # Phase-out range taxinc_less_qbid = person.tax_unit("taxable_income_less_qbid", period) filing_status = person.tax_unit("filing_status", period) po_start = p.phase_out.start[filing_status] po_length = p.phase_out.length[filing_status] - # compute phase-out limited QBID amount reduction_rate = min_( # Worksheet 12-A, line 24; Schedule A, line 9 1, (max_(0, taxinc_less_qbid - po_start)) / po_length ) applicable_rate = 1 - reduction_rate # Schedule A, line 10 - is_sstb = person("business_is_sstb", period) - # Schedule A, line 11 - sstb_multiplier = where(is_sstb, applicable_rate, 1) - adj_qbid_max = qbid_max * sstb_multiplier - # Schedule A, line 12 and line 13 - adj_cap = full_cap * sstb_multiplier - line11 = min_(adj_qbid_max, adj_cap) # Worksheet 12-A, line 11 - # compute phased reduction - reduction = reduction_rate * max_( # Worksheet 12-A, line 25 - 0, adj_qbid_max - adj_cap + total_w2_wages = person("w2_wages_from_qualified_business", period) + total_b_property = person("unadjusted_basis_qualified_property", period) + + def qbi_component(qbi, full_cap, sstb_multiplier): + # Worksheet 12-A lines 3, 11-13 / Schedule A lines 9-12. + qbid_max = p.max.rate * qbi # Worksheet 12-A, line 3 + adj_qbid_max = qbid_max * sstb_multiplier + adj_cap = full_cap * sstb_multiplier + line11 = min_(adj_qbid_max, adj_cap) + reduction = reduction_rate * max_(0, adj_qbid_max - adj_cap) + line26 = max_(0, adj_qbid_max - reduction) + line12 = where(adj_cap < adj_qbid_max, line26, 0) + return max_(line11, line12) + + # Non-SSTB and SSTB QBI categories. Backward compatibility: + # if the legacy `business_is_sstb` flag is set, route the legacy + # `qualified_business_income` into the SSTB component so the + # phaseout still applies. + non_sstb_qbi = person("qualified_business_income", period) + sstb_qbi_from_se = person("sstb_qualified_business_income", period) + is_sstb_legacy = person("business_is_sstb", period) + sstb_qbi = sstb_qbi_from_se + where(is_sstb_legacy, non_sstb_qbi, 0) + non_sstb_qbi_final = where(is_sstb_legacy, 0, non_sstb_qbi) + + has_non_sstb = non_sstb_qbi_final > 0 + has_sstb = sstb_qbi > 0 + has_mixed_categories = has_non_sstb & has_sstb + + # Schedule A applies the SSTB applicable percentage to the SSTB's own + # allocable W-2 wages and UBIA. The model stores person-level totals, + # so mixed cases use explicit SSTB allocable inputs to split those + # totals without double counting the same wage/property pool twice. + sstb_w2_wages = where( + is_sstb_legacy, + total_w2_wages, + where( + has_mixed_categories, + person("sstb_w2_wages_from_qualified_business", period), + where(has_sstb, total_w2_wages, 0), + ), + ) + non_sstb_w2_wages = where( + is_sstb_legacy, + 0, + where( + has_mixed_categories, + max_(0, total_w2_wages - sstb_w2_wages), + where(has_non_sstb, total_w2_wages, 0), + ), + ) + + sstb_b_property = where( + is_sstb_legacy, + total_b_property, + where( + has_mixed_categories, + person("sstb_unadjusted_basis_qualified_property", period), + where(has_sstb, total_b_property, 0), + ), + ) + non_sstb_b_property = where( + is_sstb_legacy, + 0, + where( + has_mixed_categories, + max_(0, total_b_property - sstb_b_property), + where(has_non_sstb, total_b_property, 0), + ), + ) + + def full_cap(w2_wages, b_property): + wage_cap = w2_wages * p.max.w2_wages.rate # Worksheet 12-A, line 5 + alt_cap = ( # Worksheet 12-A, line 9 + w2_wages * p.max.w2_wages.alt_rate + + b_property * p.max.business_property.rate + ) + return max_(wage_cap, alt_cap) # Worksheet 12-A, line 10 + + non_sstb_component = qbi_component( + non_sstb_qbi_final, + full_cap(non_sstb_w2_wages, non_sstb_b_property), + 1, + ) + sstb_component = qbi_component( + sstb_qbi, + full_cap(sstb_w2_wages, sstb_b_property), + applicable_rate, ) - line26 = max_(0, adj_qbid_max - reduction) - line12 = where(adj_cap < adj_qbid_max, line26, 0) - # QBI component (Worksheet 12-A, line 13 / Form 8995 Line 5) - qbi_component = max_(line11, line12) - # REIT/PTP component (Form 8995 Lines 6-9) - # Per §199A(b)(1)(B), qualified REIT dividends and qualified PTP income - # receive a 20% deduction WITHOUT W-2 wage or UBIA limitations + # REIT/PTP component (Form 8995 Lines 6-9). + # Per §199A(b)(1)(B), qualified REIT dividends and qualified PTP + # income receive a 20% deduction WITHOUT W-2 wage or UBIA limitations. reit_ptp_income = person("qualified_reit_and_ptp_income", period) reit_ptp_component = p.max.reit_ptp_rate * max_(0, reit_ptp_income) - # Total QBID = QBI component + REIT/PTP component - # (Form 8995 Line 10: Add lines 5 and 9) - return qbi_component + reit_ptp_component + # Total QBID = non-SSTB + SSTB + REIT/PTP (Form 8995 Line 10). + return non_sstb_component + sstb_component + reit_ptp_component diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/qualified_business_income_deduction/qualified_business_income.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/qualified_business_income_deduction/qualified_business_income.py index 7abbfda5c87..eb62a2d769a 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/qualified_business_income_deduction/qualified_business_income.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/qualified_business_income_deduction/qualified_business_income.py @@ -6,7 +6,10 @@ class qualified_business_income(Variable): entity = Person label = "Qualified business income" documentation = ( - "Business income that qualifies for the qualified business income deduction." + "Non-SSTB business income that qualifies for the qualified business " + "income deduction. Excludes sstb_self_employment_income, which is " + "tracked separately so the §199A(d)(3) phaseout can apply only to the " + "SSTB component above the threshold." ) unit = USD definition_period = YEAR @@ -15,10 +18,23 @@ class qualified_business_income(Variable): def formula(person, period, parameters): p = parameters(period).gov.irs.deductions.qbi - gross_qbi = 0 + non_sstb_gross = 0 for var in p.income_definition: - gross_qbi += person(var, period) * person( + non_sstb_gross += person(var, period) * person( var + "_would_be_qualified", period ) + sstb_gross = person("sstb_self_employment_income", period) * person( + "sstb_self_employment_income_would_be_qualified", period + ) + # Pro-rate QBI deductions across positive non-SSTB and SSTB income + # only, so mixed-sign categories do not generate negative shares. + positive_non_sstb_gross = max_(0, non_sstb_gross) + positive_sstb_gross = max_(0, sstb_gross) + positive_gross_total = positive_non_sstb_gross + positive_sstb_gross qbi_deductions = add(person, period, p.deduction_definition) - return max_(0, gross_qbi - qbi_deductions) + non_sstb_share = where( + positive_gross_total > 0, + positive_non_sstb_gross / positive_gross_total, + 0, + ) + return max_(0, non_sstb_gross - qbi_deductions * non_sstb_share) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/qualified_business_income_deduction/qualified_business_income_deduction.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/qualified_business_income_deduction/qualified_business_income_deduction.py index e1cecf38f87..1a2c1193d94 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/qualified_business_income_deduction/qualified_business_income_deduction.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/qualified_business_income_deduction/qualified_business_income_deduction.py @@ -17,6 +17,10 @@ def formula(tax_unit, period, parameters): # logic in 2018 IRS Publication 535, Worksheet 12-A, line 16 person = tax_unit.members qbid_amt = person("qbid_amount", period) + total_qbi = tax_unit.sum( + person("qualified_business_income", period) + + person("sstb_qualified_business_income", period) + ) uncapped_qbid = tax_unit.sum(qbid_amt) # apply taxinc cap at the TaxUnit level following logic # in 2018 IRS Publication 535, Worksheet 12-A, lines 32-37 @@ -26,7 +30,6 @@ def formula(tax_unit, period, parameters): taxinc_cap = p.max.rate * max_(0, taxinc_less_qbid - netcg_qdiv) pre_floor_qbid = min_(uncapped_qbid, taxinc_cap) if p.deduction_floor.in_effect: - qualified_business_income = tax_unit("qualified_business_income", period) - floor = p.deduction_floor.amount.calc(qualified_business_income) + floor = p.deduction_floor.amount.calc(total_qbi) return max_(pre_floor_qbid, floor) return pre_floor_qbid diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/deductions/qualified_business_income_deduction/sstb_qualified_business_income.py b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/qualified_business_income_deduction/sstb_qualified_business_income.py new file mode 100644 index 00000000000..24e325ea103 --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/deductions/qualified_business_income_deduction/sstb_qualified_business_income.py @@ -0,0 +1,42 @@ +from policyengine_us.model_api import * + + +class sstb_qualified_business_income(Variable): + value_type = float + entity = Person + label = "SSTB qualified business income" + documentation = ( + "Qualified business income from a specified service trade or business " + "(SSTB) under IRC §199A(d)(2). Tracked separately from non-SSTB QBI so " + "the §199A(d)(3) applicable-percentage phaseout above the threshold can " + "reduce only the SSTB component." + ) + unit = USD + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/199A#c", + "https://www.law.cornell.edu/uscode/text/26/199A#d_2", + ) + + def formula(person, period, parameters): + p = parameters(period).gov.irs.deductions.qbi + non_sstb_gross = 0 + for var in p.income_definition: + non_sstb_gross += person(var, period) * person( + var + "_would_be_qualified", period + ) + sstb_gross = person("sstb_self_employment_income", period) * person( + "sstb_self_employment_income_would_be_qualified", period + ) + # Pro-rate QBI deductions across positive non-SSTB and SSTB income so + # that mixed-sign categories do not generate negative shares. + positive_non_sstb_gross = max_(0, non_sstb_gross) + positive_sstb_gross = max_(0, sstb_gross) + positive_gross_total = positive_non_sstb_gross + positive_sstb_gross + qbi_deductions = add(person, period, p.deduction_definition) + sstb_share = where( + positive_gross_total > 0, + positive_sstb_gross / positive_gross_total, + 0, + ) + return max_(0, sstb_gross - qbi_deductions * sstb_share) diff --git a/policyengine_us/variables/gov/irs/tax/self_employment/taxable_self_employment_income.py b/policyengine_us/variables/gov/irs/tax/self_employment/taxable_self_employment_income.py index 1f44a6f5640..38dc9ec4f61 100644 --- a/policyengine_us/variables/gov/irs/tax/self_employment/taxable_self_employment_income.py +++ b/policyengine_us/variables/gov/irs/tax/self_employment/taxable_self_employment_income.py @@ -11,12 +11,13 @@ class taxable_self_employment_income(Variable): def formula(person, period, parameters): # Per 26 USC 1402(a), SE income includes: - # - Schedule C net profit (self_employment_income) + # - Schedule C net profit (self_employment_income, sstb_self_employment_income) # - Schedule F net profit (farm_income) # - General partners' distributive share (partnership_se_income from K-1 Box 14) # S-corp distributions are NOT subject to SE tax. SEI_SOURCES = [ "self_employment_income", + "sstb_self_employment_income", "farm_income", "partnership_se_income", ] diff --git a/policyengine_us/variables/gov/local/ca/riv/general_relief/eligibility/ca_riv_general_relief_meets_work_requirements.py b/policyengine_us/variables/gov/local/ca/riv/general_relief/eligibility/ca_riv_general_relief_meets_work_requirements.py index 8fcb3d1f81a..6f75782c5fb 100644 --- a/policyengine_us/variables/gov/local/ca/riv/general_relief/eligibility/ca_riv_general_relief_meets_work_requirements.py +++ b/policyengine_us/variables/gov/local/ca/riv/general_relief/eligibility/ca_riv_general_relief_meets_work_requirements.py @@ -12,7 +12,16 @@ def formula(person, period, parameters): p = parameters(period).gov.local.ca.riv.general_relief.work_exempted_age # Person who is actively searching for jobs also qualify for work requirements is_working = ( - add(person, period, ["employment_income", "self_employment_income"]) > 0 + add( + person, + period, + [ + "employment_income", + "self_employment_income", + "sstb_self_employment_income", + ], + ) + > 0 ) # Check if person is a qualifying secondary school student age = person("monthly_age", period) diff --git a/policyengine_us/variables/gov/simulation/behavioral_response_measurements.py b/policyengine_us/variables/gov/simulation/behavioral_response_measurements.py index 60240a3d2a2..540be551369 100644 --- a/policyengine_us/variables/gov/simulation/behavioral_response_measurements.py +++ b/policyengine_us/variables/gov/simulation/behavioral_response_measurements.py @@ -8,11 +8,13 @@ NEUTRALIZED_BEHAVIORAL_RESPONSE_VARIABLES = ( "employment_income_behavioral_response", "self_employment_income_behavioral_response", + "sstb_self_employment_income_behavioral_response", "capital_gains_behavioral_response", ) BEHAVIORAL_RESPONSE_INPUT_VARIABLES = ( "employment_income_before_lsr", "self_employment_income_before_lsr", + "sstb_self_employment_income_before_lsr", "long_term_capital_gains_before_response", ) @@ -90,15 +92,14 @@ def get_behavioral_response_measurements(person, period): # pragma: no cover def earnings_before_lsr(person, period): - raw_earnings = add( - person, - period, - [ - "employment_income_before_lsr", - "self_employment_income_before_lsr", - ], + employment_income = max_(person("employment_income_before_lsr", period), 0) + self_employment_income = abs( + person("self_employment_income_before_lsr", period) ) - return max_(raw_earnings, 0) + sstb_self_employment_income = abs( + person("sstb_self_employment_income_before_lsr", period) + ) + return employment_income + self_employment_income + sstb_self_employment_income def calculate_relative_income_change(measurements, bounds): diff --git a/policyengine_us/variables/gov/simulation/labor_supply_response/employment_income_behavioral_response.py b/policyengine_us/variables/gov/simulation/labor_supply_response/employment_income_behavioral_response.py index fa129addcb8..7134fa11ac0 100644 --- a/policyengine_us/variables/gov/simulation/labor_supply_response/employment_income_behavioral_response.py +++ b/policyengine_us/variables/gov/simulation/labor_supply_response/employment_income_behavioral_response.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.simulation.behavioral_response_measurements import ( + earnings_before_lsr, +) class employment_income_behavioral_response(Variable): @@ -10,17 +13,9 @@ class employment_income_behavioral_response(Variable): def formula(person, period, parameters): lsr = person("labor_supply_behavioral_response", period) - raw_earnings = add( - person, - period, - [ - "employment_income_before_lsr", - "self_employment_income_before_lsr", - ], - ) - earnings = max_(raw_earnings, 0) - employment_income = person("employment_income_before_lsr", period) - emp_share = np.ones_like(earnings) - mask = earnings > 0 - emp_share[mask] = employment_income[mask] / earnings[mask] + employment_income = max_(person("employment_income_before_lsr", period), 0) + total_earnings = earnings_before_lsr(person, period) + emp_share = np.ones_like(total_earnings) + mask = total_earnings > 0 + emp_share[mask] = employment_income[mask] / total_earnings[mask] return lsr * emp_share diff --git a/policyengine_us/variables/gov/simulation/labor_supply_response/self_employment_income_behavioral_response.py b/policyengine_us/variables/gov/simulation/labor_supply_response/self_employment_income_behavioral_response.py index 25a798a7243..33be578b892 100644 --- a/policyengine_us/variables/gov/simulation/labor_supply_response/self_employment_income_behavioral_response.py +++ b/policyengine_us/variables/gov/simulation/labor_supply_response/self_employment_income_behavioral_response.py @@ -7,5 +7,23 @@ class self_employment_income_behavioral_response(Variable): label = "self-employment income behavioral response" unit = USD definition_period = YEAR - adds = ["labor_supply_behavioral_response"] - subtracts = ["employment_income_behavioral_response"] + + def formula(person, period, parameters): + lsr = person("labor_supply_behavioral_response", period) + employment_response = person("employment_income_behavioral_response", period) + total_self_employment_response = lsr - employment_response + non_sstb_self_employment_income = abs( + person("self_employment_income_before_lsr", period) + ) + sstb_self_employment_income = abs( + person("sstb_self_employment_income_before_lsr", period) + ) + total_self_employment_income = ( + non_sstb_self_employment_income + sstb_self_employment_income + ) + non_sstb_share = where( + total_self_employment_income > 0, + non_sstb_self_employment_income / total_self_employment_income, + 1, + ) + return total_self_employment_response * non_sstb_share diff --git a/policyengine_us/variables/gov/simulation/labor_supply_response/sstb_self_employment_income_behavioral_response.py b/policyengine_us/variables/gov/simulation/labor_supply_response/sstb_self_employment_income_behavioral_response.py new file mode 100644 index 00000000000..63f62aaf43f --- /dev/null +++ b/policyengine_us/variables/gov/simulation/labor_supply_response/sstb_self_employment_income_behavioral_response.py @@ -0,0 +1,29 @@ +from policyengine_us.model_api import * + + +class sstb_self_employment_income_behavioral_response(Variable): + value_type = float + entity = Person + label = "SSTB self-employment income behavioral response" + unit = USD + definition_period = YEAR + + def formula(person, period, parameters): + lsr = person("labor_supply_behavioral_response", period) + employment_response = person("employment_income_behavioral_response", period) + total_self_employment_response = lsr - employment_response + non_sstb_self_employment_income = abs( + person("self_employment_income_before_lsr", period) + ) + sstb_self_employment_income = abs( + person("sstb_self_employment_income_before_lsr", period) + ) + total_self_employment_income = ( + non_sstb_self_employment_income + sstb_self_employment_income + ) + sstb_share = where( + total_self_employment_income > 0, + sstb_self_employment_income / total_self_employment_income, + 0, + ) + return total_self_employment_response * sstb_share diff --git a/policyengine_us/variables/gov/simulation/labor_supply_response/substitution_elasticity.py b/policyengine_us/variables/gov/simulation/labor_supply_response/substitution_elasticity.py index 951c4433c84..80243873fbc 100644 --- a/policyengine_us/variables/gov/simulation/labor_supply_response/substitution_elasticity.py +++ b/policyengine_us/variables/gov/simulation/labor_supply_response/substitution_elasticity.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.simulation.behavioral_response_measurements import ( + earnings_before_lsr, +) class substitution_elasticity(Variable): @@ -30,15 +33,7 @@ def formula(person, period, parameters): 1_726e3, ] - raw_earnings = add( - person, - period, - [ - "employment_income_before_lsr", - "self_employment_income_before_lsr", - ], - ) - earnings = max_(raw_earnings, 0) + earnings = earnings_before_lsr(person, period) earnings_decile = np.searchsorted(EARNINGS_DECILE_MARKERS, earnings) + 1 tax_unit = person.tax_unit diff --git a/policyengine_us/variables/gov/states/dc/dhs/ccsp/eligibility/qualified_activity_or_need/dc_ccsp_qualified_activity_eligible.py b/policyengine_us/variables/gov/states/dc/dhs/ccsp/eligibility/qualified_activity_or_need/dc_ccsp_qualified_activity_eligible.py index 5f244a77c16..8644eda9592 100644 --- a/policyengine_us/variables/gov/states/dc/dhs/ccsp/eligibility/qualified_activity_or_need/dc_ccsp_qualified_activity_eligible.py +++ b/policyengine_us/variables/gov/states/dc/dhs/ccsp/eligibility/qualified_activity_or_need/dc_ccsp_qualified_activity_eligible.py @@ -18,7 +18,11 @@ def formula(spm_unit, period, parameters): add( person, period, - ["employment_income", "self_employment_income"], + [ + "employment_income", + "self_employment_income", + "sstb_self_employment_income", + ], ) > 0 ) diff --git a/policyengine_us/variables/gov/states/dc/tax/income/additions/dc_self_employment_loss_addition.py b/policyengine_us/variables/gov/states/dc/tax/income/additions/dc_self_employment_loss_addition.py index 657ead6401b..998e9695a38 100644 --- a/policyengine_us/variables/gov/states/dc/tax/income/additions/dc_self_employment_loss_addition.py +++ b/policyengine_us/variables/gov/states/dc/tax/income/additions/dc_self_employment_loss_addition.py @@ -14,7 +14,7 @@ class dc_self_employment_loss_addition(Variable): defined_for = StateCode.DC def formula(person, period, parameters): - loss_person = max_(0, -person("self_employment_income", period)) + loss_person = max_(0, -person("total_self_employment_income", period)) loss_taxunit = person.tax_unit.sum(loss_person) # Cap at SE loss actually deducted in federal AGI via loss_ald. # loss_ald includes both SE and capital losses; isolate SE portion. diff --git a/policyengine_us/variables/gov/states/de/dss/poc/eligibility/de_poc_activity_eligible.py b/policyengine_us/variables/gov/states/de/dss/poc/eligibility/de_poc_activity_eligible.py index 56bfb00ffb6..dc47b42cee1 100644 --- a/policyengine_us/variables/gov/states/de/dss/poc/eligibility/de_poc_activity_eligible.py +++ b/policyengine_us/variables/gov/states/de/dss/poc/eligibility/de_poc_activity_eligible.py @@ -14,7 +14,12 @@ def formula(spm_unit, period, parameters): is_head_or_spouse = person("is_tax_unit_head_or_spouse", period.this_year) # Employment (DSSM 11003) has_employment = (person("employment_income", period) > 0) | ( - person("self_employment_income", period) > 0 + add( + person, + period, + ["self_employment_income", "sstb_self_employment_income"], + ) + > 0 ) # Education / training (DSSM 11003) is_student = person("is_full_time_student", period.this_year) diff --git a/policyengine_us/variables/gov/states/il/dhs/ccap/eligibility/il_ccap_parent_meets_working_requirements.py b/policyengine_us/variables/gov/states/il/dhs/ccap/eligibility/il_ccap_parent_meets_working_requirements.py index 9a5bce23abe..f8d3f81a514 100644 --- a/policyengine_us/variables/gov/states/il/dhs/ccap/eligibility/il_ccap_parent_meets_working_requirements.py +++ b/policyengine_us/variables/gov/states/il/dhs/ccap/eligibility/il_ccap_parent_meets_working_requirements.py @@ -16,7 +16,11 @@ def formula(spm_unit, period, parameters): add( person, period, - ["employment_income", "self_employment_income"], + [ + "employment_income", + "self_employment_income", + "sstb_self_employment_income", + ], ) > 0 ) diff --git a/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_gross_income_loss_adjustment.py b/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_gross_income_loss_adjustment.py index fc7251ba9b5..e3e6b78838f 100644 --- a/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_gross_income_loss_adjustment.py +++ b/policyengine_us/variables/gov/states/ma/tax/income/gross_income/ma_gross_income_loss_adjustment.py @@ -17,7 +17,7 @@ def formula(tax_unit, period, parameters): # Line 10 instruction: "Be sure to subtract any losses # in lines 6 or 7." # Line 6a: Business/profession loss (Schedule C) - se_income = add(tax_unit, period, ["self_employment_income"]) + se_income = add(tax_unit, period, ["total_self_employment_income"]) # Line 6b: Farm loss (Schedule F) farm = add(tax_unit, period, ["farm_income"]) # Line 7: Rental, partnership, S-corp, farm rent losses diff --git a/policyengine_us/variables/gov/states/mi/tax/income/mi_household_resources.py b/policyengine_us/variables/gov/states/mi/tax/income/mi_household_resources.py index 24984404545..9219966b13e 100644 --- a/policyengine_us/variables/gov/states/mi/tax/income/mi_household_resources.py +++ b/policyengine_us/variables/gov/states/mi/tax/income/mi_household_resources.py @@ -27,7 +27,7 @@ def formula(tax_unit, period, parameters): # "Net royalty or rent income. If negative, enter 0" floored_sources = { "farm_income", - "self_employment_income", + "total_self_employment_income", "partnership_s_corp_income", "rental_income", "farm_rent_income", diff --git a/policyengine_us/variables/gov/states/mo/tax/income/deductions/mo_business_income_deduction.py b/policyengine_us/variables/gov/states/mo/tax/income/deductions/mo_business_income_deduction.py index 72774ffadee..8aeb4eeff4c 100644 --- a/policyengine_us/variables/gov/states/mo/tax/income/deductions/mo_business_income_deduction.py +++ b/policyengine_us/variables/gov/states/mo/tax/income/deductions/mo_business_income_deduction.py @@ -15,6 +15,9 @@ class mo_business_income_deduction(Variable): def formula(tax_unit, period, parameters): p = parameters(period).gov.states.mo.tax.income.deductions.business_income - qualified_business_income = add(tax_unit, period, ["qualified_business_income"]) - total_qualified_business_income = tax_unit.sum(qualified_business_income) - return p.rate * qualified_business_income + person = tax_unit.members + total_qualified_business_income = tax_unit.sum( + person("qualified_business_income", period) + + person("sstb_qualified_business_income", period) + ) + return p.rate * total_qualified_business_income diff --git a/policyengine_us/variables/gov/states/nd/tax/income/credits/nd_mpc.py b/policyengine_us/variables/gov/states/nd/tax/income/credits/nd_mpc.py index f8427b4ddc5..47a9348ddba 100644 --- a/policyengine_us/variables/gov/states/nd/tax/income/credits/nd_mpc.py +++ b/policyengine_us/variables/gov/states/nd/tax/income/credits/nd_mpc.py @@ -28,7 +28,7 @@ def formula(tax_unit, period, parameters): # determine minimum qualified income between head and spouse qinc_sources = [ "irs_employment_income", - "self_employment_income", + "total_self_employment_income", "taxable_pension_income", ] person = tax_unit.members diff --git a/policyengine_us/variables/gov/states/vt/dcf/ccfap/eligibility/vt_ccfap_meets_activity_test.py b/policyengine_us/variables/gov/states/vt/dcf/ccfap/eligibility/vt_ccfap_meets_activity_test.py index 7359725cd4f..13fbed91af9 100644 --- a/policyengine_us/variables/gov/states/vt/dcf/ccfap/eligibility/vt_ccfap_meets_activity_test.py +++ b/policyengine_us/variables/gov/states/vt/dcf/ccfap/eligibility/vt_ccfap_meets_activity_test.py @@ -14,7 +14,9 @@ def formula(spm_unit, period): is_adult = person("is_adult", period) # Employment (II B 1 a) or Self Employment (II B 1 b) has_employment = person("employment_income", period.this_year) > 0 - has_self_employment = person("self_employment_income", period.this_year) > 0 + has_self_employment = ( + person("total_self_employment_income", period.this_year) > 0 + ) employed = spm_unit.any(is_adult & (has_employment | has_self_employment)) # Training or Education (II B 1 e) in_training = spm_unit.any( diff --git a/policyengine_us/variables/gov/states/vt/tax/income/vt_child_care_contributions.py b/policyengine_us/variables/gov/states/vt/tax/income/vt_child_care_contributions.py index dbc45959532..d3e5b529b46 100644 --- a/policyengine_us/variables/gov/states/vt/tax/income/vt_child_care_contributions.py +++ b/policyengine_us/variables/gov/states/vt/tax/income/vt_child_care_contributions.py @@ -12,7 +12,7 @@ class vt_child_care_contributions(Variable): def formula(tax_unit, period, parameters): p = parameters(period).gov.states.vt.tax.income.child_care_contributions if p.applies: - income = add(tax_unit, period, ["self_employment_income"]) + income = add(tax_unit, period, ["total_self_employment_income"]) applicable_income = max_(0, income) * p.rate.income return applicable_income * p.rate.contributions return 0 diff --git a/policyengine_us/variables/gov/usda/snap/income/deductions/self_employment/snap_self_employment_expense_deduction.py b/policyengine_us/variables/gov/usda/snap/income/deductions/self_employment/snap_self_employment_expense_deduction.py index ef60a137ba8..f78dab19001 100644 --- a/policyengine_us/variables/gov/usda/snap/income/deductions/self_employment/snap_self_employment_expense_deduction.py +++ b/policyengine_us/variables/gov/usda/snap/income/deductions/self_employment/snap_self_employment_expense_deduction.py @@ -14,7 +14,12 @@ class snap_self_employment_expense_deduction(Variable): def formula(spm_unit, period, parameters): self_employment_income = add( - spm_unit, period, ["self_employment_income_before_lsr"] + spm_unit, + period, + [ + "self_employment_income_before_lsr", + "sstb_self_employment_income_before_lsr", + ], ) expenses = spm_unit("snap_self_employment_income_expense", period) p = parameters(period).gov.usda.snap.income.deductions.self_employment diff --git a/policyengine_us/variables/gov/usda/snap/income/deductions/self_employment/snap_self_employment_income_after_expense_deduction.py b/policyengine_us/variables/gov/usda/snap/income/deductions/self_employment/snap_self_employment_income_after_expense_deduction.py index a7f47cc0adf..eee469b1c9b 100644 --- a/policyengine_us/variables/gov/usda/snap/income/deductions/self_employment/snap_self_employment_income_after_expense_deduction.py +++ b/policyengine_us/variables/gov/usda/snap/income/deductions/self_employment/snap_self_employment_income_after_expense_deduction.py @@ -10,7 +10,12 @@ class snap_self_employment_income_after_expense_deduction(Variable): def formula(spm_unit, period, parameters): self_employment_income = add( - spm_unit, period, ["self_employment_income_before_lsr"] + spm_unit, + period, + [ + "self_employment_income_before_lsr", + "sstb_self_employment_income_before_lsr", + ], ) expense_deduction = spm_unit("snap_self_employment_expense_deduction", period) return max_(self_employment_income - expense_deduction, 0) diff --git a/policyengine_us/variables/household/emp_self_emp_ratio.py b/policyengine_us/variables/household/emp_self_emp_ratio.py index 9edd6b2795b..b904346f1d1 100644 --- a/policyengine_us/variables/household/emp_self_emp_ratio.py +++ b/policyengine_us/variables/household/emp_self_emp_ratio.py @@ -10,9 +10,16 @@ class emp_self_emp_ratio(Variable): reference = "https://www.law.cornell.edu/uscode/text/26/1402" def formula(person, period, parameters): - employment_income = person("employment_income", period) - self_employment_income = person("self_employment_income", period) - earnings = employment_income + self_employment_income + employment_income = max_(0, person("employment_income", period)) + self_employment_income = max_(0, person("self_employment_income", period)) + sstb_self_employment_income = max_( + 0, person("sstb_self_employment_income", period) + ) + earnings = ( + employment_income + + self_employment_income + + sstb_self_employment_income + ) res = np.ones_like(earnings) mask = earnings > 0 res[mask] = employment_income[mask] / earnings[mask] diff --git a/policyengine_us/variables/household/income/person/general/market_income.py b/policyengine_us/variables/household/income/person/general/market_income.py index b21a8195400..4d9bf69ef12 100644 --- a/policyengine_us/variables/household/income/person/general/market_income.py +++ b/policyengine_us/variables/household/income/person/general/market_income.py @@ -13,6 +13,7 @@ def formula(person, period, parameters): COMPONENTS = [ "employment_income", "self_employment_income", + "sstb_self_employment_income", "pension_income", "dividend_income", "interest_income", diff --git a/policyengine_us/variables/household/income/person/self_employment/sstb_self_employment_income_would_be_qualified.py b/policyengine_us/variables/household/income/person/self_employment/sstb_self_employment_income_would_be_qualified.py new file mode 100644 index 00000000000..ba0bbc4b2b3 --- /dev/null +++ b/policyengine_us/variables/household/income/person/self_employment/sstb_self_employment_income_would_be_qualified.py @@ -0,0 +1,14 @@ +from policyengine_us.model_api import * + + +class sstb_self_employment_income_would_be_qualified(Variable): + value_type = bool + entity = Person + label = "SSTB self-employment income would be qualified" + documentation = ( + "Whether SSTB self-employment income would count toward qualified " + "business income before the §199A(d)(3) applicable-percentage phaseout." + ) + definition_period = YEAR + reference = "https://www.law.cornell.edu/uscode/text/26/199A#c_3_A" + default_value = True diff --git a/policyengine_us/variables/household/income/person/self_employment/sstb_unadjusted_basis_qualified_property.py b/policyengine_us/variables/household/income/person/self_employment/sstb_unadjusted_basis_qualified_property.py new file mode 100644 index 00000000000..0995258b96c --- /dev/null +++ b/policyengine_us/variables/household/income/person/self_employment/sstb_unadjusted_basis_qualified_property.py @@ -0,0 +1,21 @@ +from policyengine_us.model_api import * + + +class sstb_unadjusted_basis_qualified_property(Variable): + value_type = float + entity = Person + label = "SSTB allocable UBIA of qualified property" + unit = USD + documentation = ( + "Portion of unadjusted_basis_qualified_property allocable to " + "specified service trades or businesses for section 199A. Used to " + "apply the UBIA limitation separately to SSTB and non-SSTB " + "categories in mixed-business cases." + ) + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/199A#b_2", + "https://www.law.cornell.edu/uscode/text/26/199A#d_3", + "https://www.irs.gov/pub/irs-pdf/f8995aa.pdf", + ) + default_value = 0 diff --git a/policyengine_us/variables/household/income/person/self_employment/sstb_w2_wages_from_qualified_business.py b/policyengine_us/variables/household/income/person/self_employment/sstb_w2_wages_from_qualified_business.py new file mode 100644 index 00000000000..53509d7e8a5 --- /dev/null +++ b/policyengine_us/variables/household/income/person/self_employment/sstb_w2_wages_from_qualified_business.py @@ -0,0 +1,21 @@ +from policyengine_us.model_api import * + + +class sstb_w2_wages_from_qualified_business(Variable): + value_type = float + entity = Person + label = "SSTB allocable W-2 wages" + unit = USD + documentation = ( + "Portion of w2_wages_from_qualified_business allocable to specified " + "service trades or businesses for section 199A. Used to apply the " + "W-2 wage limitation separately to SSTB and non-SSTB categories in " + "mixed-business cases." + ) + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/199A#b_2", + "https://www.law.cornell.edu/uscode/text/26/199A#d_3", + "https://www.irs.gov/pub/irs-pdf/f8995aa.pdf", + ) + default_value = 0 diff --git a/policyengine_us/variables/household/income/person/self_employment/total_self_employment_income.py b/policyengine_us/variables/household/income/person/self_employment/total_self_employment_income.py new file mode 100644 index 00000000000..c5699055e0d --- /dev/null +++ b/policyengine_us/variables/household/income/person/self_employment/total_self_employment_income.py @@ -0,0 +1,16 @@ +from policyengine_us.model_api import * + + +class total_self_employment_income(Variable): + value_type = float + entity = Person + label = "total self-employment income" + unit = USD + documentation = ( + "Total non-farm self-employment income, including both SSTB and " + "non-SSTB Schedule C income." + ) + definition_period = YEAR + adds = ["self_employment_income", "sstb_self_employment_income"] + reference = "https://www.law.cornell.edu/uscode/text/26/1402#a" + uprating = "calibration.gov.irs.soi.self_employment_income" diff --git a/policyengine_us/variables/household/income/person/weekly_hours_worked.py b/policyengine_us/variables/household/income/person/weekly_hours_worked.py index c88601549f7..9781847491f 100644 --- a/policyengine_us/variables/household/income/person/weekly_hours_worked.py +++ b/policyengine_us/variables/household/income/person/weekly_hours_worked.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.simulation.behavioral_response_measurements import ( + earnings_before_lsr, +) class weekly_hours_worked(Variable): @@ -39,9 +42,7 @@ def formula(person, period, parameters): else: income_effect = np.zeros_like(original) - original_emp = person("employment_income_before_lsr", period) - original_self_emp = person("self_employment_income_before_lsr", period) - original_earnings = original_emp + original_self_emp + original_earnings = earnings_before_lsr(person, period) lsr_relative_change = np.divide( income_effect, @@ -68,9 +69,7 @@ def formula(person, period, parameters): substitution_effect = person("substitution_elasticity_lsr", period) else: substitution_effect = np.zeros_like(original) - original_emp = person("employment_income_before_lsr", period) - original_self_emp = person("self_employment_income_before_lsr", period) - original_earnings = original_emp + original_self_emp + original_earnings = earnings_before_lsr(person, period) lsr_relative_change = np.divide( substitution_effect, diff --git a/policyengine_us/variables/household/marginal_tax_rate.py b/policyengine_us/variables/household/marginal_tax_rate.py index 4315dc1c773..b77bd5e143d 100644 --- a/policyengine_us/variables/household/marginal_tax_rate.py +++ b/policyengine_us/variables/household/marginal_tax_rate.py @@ -20,7 +20,25 @@ def formula(person, period, parameters): adult_indexes = person("adult_earnings_index", period) employment_income = person("employment_income", period) self_employment_income = person("self_employment_income", period) + sstb_self_employment_income = person("sstb_self_employment_income", period) emp_self_emp_ratio = person("emp_self_emp_ratio", period) + positive_self_employment_income = max_(0, self_employment_income) + positive_sstb_self_employment_income = max_( + 0, sstb_self_employment_income + ) + positive_self_employment_total = ( + positive_self_employment_income + positive_sstb_self_employment_income + ) + non_sstb_share = where( + positive_self_employment_total > 0, + positive_self_employment_income / positive_self_employment_total, + 1, + ) + sstb_share = where( + positive_self_employment_total > 0, + positive_sstb_self_employment_income / positive_self_employment_total, + 0, + ) for adult_index in range(1, 1 + adult_count): alt_sim = sim.get_branch(f"mtr_for_adult_{adult_index}") @@ -36,10 +54,16 @@ def formula(person, period, parameters): period, employment_income + mask * delta * emp_self_emp_ratio, ) + self_employment_delta = mask * delta * (1 - emp_self_emp_ratio) alt_sim.set_input( "self_employment_income", period, - self_employment_income + mask * delta * (1 - emp_self_emp_ratio), + self_employment_income + self_employment_delta * non_sstb_share, + ) + alt_sim.set_input( + "sstb_self_employment_income", + period, + sstb_self_employment_income + self_employment_delta * sstb_share, ) alt_person = alt_sim.person netinc_alt = alt_person.household("household_net_income", period) diff --git a/policyengine_us/variables/household/marginal_tax_rate_including_health_benefits.py b/policyengine_us/variables/household/marginal_tax_rate_including_health_benefits.py index 8de84ed69c2..d6877f1068f 100644 --- a/policyengine_us/variables/household/marginal_tax_rate_including_health_benefits.py +++ b/policyengine_us/variables/household/marginal_tax_rate_including_health_benefits.py @@ -22,7 +22,25 @@ def formula(person, period, parameters): adult_indexes = person("adult_earnings_index", period) employment_income = person("employment_income", period) self_employment_income = person("self_employment_income", period) + sstb_self_employment_income = person("sstb_self_employment_income", period) emp_self_emp_ratio = person("emp_self_emp_ratio", period) + positive_self_employment_income = max_(0, self_employment_income) + positive_sstb_self_employment_income = max_( + 0, sstb_self_employment_income + ) + positive_self_employment_total = ( + positive_self_employment_income + positive_sstb_self_employment_income + ) + non_sstb_share = where( + positive_self_employment_total > 0, + positive_self_employment_income / positive_self_employment_total, + 1, + ) + sstb_share = where( + positive_self_employment_total > 0, + positive_sstb_self_employment_income / positive_self_employment_total, + 0, + ) for adult_index in range(1, 1 + adult_count): alt_sim = sim.get_branch(f"mtr_for_adult_{adult_index}") @@ -38,10 +56,16 @@ def formula(person, period, parameters): period, employment_income + mask * delta * emp_self_emp_ratio, ) + self_employment_delta = mask * delta * (1 - emp_self_emp_ratio) alt_sim.set_input( "self_employment_income", period, - self_employment_income + mask * delta * (1 - emp_self_emp_ratio), + self_employment_income + self_employment_delta * non_sstb_share, + ) + alt_sim.set_input( + "sstb_self_employment_income", + period, + sstb_self_employment_income + self_employment_delta * sstb_share, ) alt_person = alt_sim.person netinc_alt = alt_person.household( diff --git a/policyengine_us/variables/input/sstb_self_employment_income.py b/policyengine_us/variables/input/sstb_self_employment_income.py new file mode 100644 index 00000000000..c0cab37c3ad --- /dev/null +++ b/policyengine_us/variables/input/sstb_self_employment_income.py @@ -0,0 +1,26 @@ +from policyengine_us.model_api import * + + +class sstb_self_employment_income(Variable): + value_type = float + entity = Person + label = "SSTB self-employment income" + unit = USD + documentation = ( + "Self-employment non-farm income from a specified service trade or " + "business (SSTB) under IRC §199A(d)(2). Subject to SECA tax. For the " + "qualified business income deduction, this income is treated separately " + "from non-SSTB self-employment income because the SSTB applicable " + "percentage phaseout above the threshold can fully eliminate the " + "deduction without affecting non-SSTB QBI." + ) + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/1402#a", + "https://www.law.cornell.edu/uscode/text/26/199A#d_2", + ) + adds = [ + "sstb_self_employment_income_before_lsr", + "sstb_self_employment_income_behavioral_response", + ] + uprating = "calibration.gov.irs.soi.self_employment_income" diff --git a/policyengine_us/variables/input/sstb_self_employment_income_before_lsr.py b/policyengine_us/variables/input/sstb_self_employment_income_before_lsr.py new file mode 100644 index 00000000000..f2d5f62f0a7 --- /dev/null +++ b/policyengine_us/variables/input/sstb_self_employment_income_before_lsr.py @@ -0,0 +1,17 @@ +from policyengine_us.model_api import * + + +class sstb_self_employment_income_before_lsr(Variable): + value_type = float + entity = Person + label = "SSTB self-employment income before labor supply responses" + unit = USD + documentation = ( + "SSTB self-employment non-farm income before labor supply responses." + ) + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/1402#a", + "https://www.law.cornell.edu/uscode/text/26/199A#d_2", + ) + uprating = "calibration.gov.irs.soi.self_employment_income"