diff --git a/changelog.d/niit-farm-rent.fixed.md b/changelog.d/niit-farm-rent.fixed.md new file mode 100644 index 00000000000..6825bb85136 --- /dev/null +++ b/changelog.d/niit-farm-rent.fixed.md @@ -0,0 +1 @@ +Count Form 4835 farm rental income (Schedule E line 40) in net investment income, and count rental, farm rental, estate and passive pass-through losses in net investment income only as far as section 461(l) allows them, for the net investment income tax and Minnesota's net investment income tax. diff --git a/policyengine_us/parameters/gov/irs/investment/income/sources.yaml b/policyengine_us/parameters/gov/irs/investment/income/sources.yaml index 9b1f6303618..eb007febb5e 100644 --- a/policyengine_us/parameters/gov/irs/investment/income/sources.yaml +++ b/policyengine_us/parameters/gov/irs/investment/income/sources.yaml @@ -10,6 +10,17 @@ values: - taxable_interest_income - dividend_income - rental_income + # Form 4835 net farm rental income or loss goes on Schedule E line 40, + # then into line 41 and Schedule 1 line 5, which Form 8960 line 4a takes. + # Form 4835 is only for a rental activity under the passive activity + # rules, and its income isn't subject to self-employment tax, so it is + # rent under § 1411(c)(1)(A)(i). Form 8960 line 4b removes a rental's net + # income or loss only when the rental is nonpassive and rises to a section + # 162 trade or business (for example a real estate professional's rental, + # or property rented to the owner's own nonpassive business). The model + # has no input for that, here or for rental_income, so it counts all of + # both. + - farm_rent_income - passive_partnership_s_corp_income # Income from estates and trusts (Schedule E Part III) keeps its character # in the beneficiary's hands (§§ 652(b), 662(b)). Form 8960 line 4a @@ -25,6 +36,13 @@ values: # (line 13), limited under § 1211(b). Distributions reported without # Schedule D are the same amount. - filer_loss_limited_net_capital_gains + # The losses above count in full, but section 461(l) disallows business + # losses beyond business income plus a threshold, and § 1411(c)(1)(B) + # allows only deductions allowed by chapter 1. This adds back the part of + # the excess business loss that falls on those losses (all of it when they + # are the only business losses; provisionally a proportional share + # otherwise). + - net_investment_income_excess_business_loss metadata: unit: currency-USD label: net investment income sources @@ -49,3 +67,17 @@ metadata: href: https://www.irs.gov/pub/irs-prior/i8960--2024.pdf#page=11 - title: 2025 Instructions for Form 8814, Net Investment Income Tax (NIIT) href: https://www.irs.gov/pub/irs-pdf/i8814.pdf#page=2 + - title: 26 U.S. Code § 1411(c)(2) - Trades and businesses to which tax applies + href: https://www.law.cornell.edu/uscode/text/26/1411#c_2 + - title: "2025 Instructions for Form 8960, passive activity rules and lines 4a and 4b #page 4,8" + href: https://www.irs.gov/pub/irs-prior/i8960--2025.pdf#page=4 + - title: 2025 Form 4835, Farm Rental Income and Expenses, purpose of form + href: https://www.irs.gov/pub/irs-prior/f4835--2025.pdf#page=2 + - title: 2025 Schedule E (Form 1040), lines 40 and 41 + href: https://www.irs.gov/pub/irs-prior/f1040se--2025.pdf#page=2 + - title: 26 U.S. Code § 1411(c)(1)(B) - Deductions allowed by this subtitle + href: https://www.law.cornell.edu/uscode/text/26/1411#c_1_B + - title: 26 U.S. Code § 461(l) - Limitation on excess business losses of noncorporate taxpayers + href: https://www.law.cornell.edu/uscode/text/26/461#l + - title: Treas. Reg. 1.1411-4(f) - Properly allocable deductions + href: https://www.ecfr.gov/current/title-26/section-1.1411-4#p-1.1411-4(f) diff --git a/policyengine_us/tests/core/test_dependent_net_investment_income_invariants.py b/policyengine_us/tests/core/test_dependent_net_investment_income_invariants.py index f9c54139074..a96c40717c1 100644 --- a/policyengine_us/tests/core/test_dependent_net_investment_income_invariants.py +++ b/policyengine_us/tests/core/test_dependent_net_investment_income_invariants.py @@ -17,16 +17,22 @@ filer's `net_investment_income`, `filer_loss_limited_net_capital_gains`, AGI, NIIT MAGI or `net_investment_income_tax`. 2. `net_investment_income` equals an independent numpy computation over the - head and spouse: their interest, dividends, rents, passive pass-through, - estate and trust income, plus their netted Schedule D gain or loss with - capital gain distributions on line 13, limited under 26 USC 1211(b). + head and spouse: their interest, dividends, rents (including Form 4835 + farm rent), passive pass-through, estate and trust income, plus their + netted Schedule D gain or loss with capital gain distributions on line 13, + limited under 26 USC 1211(b), plus the part of their section 461(l) + excess business loss that falls on those losses. 3. -loss limit <= `filer_loss_limited_net_capital_gains`, and it equals `loss_limited_net_capital_gains` when the unit has no dependents. 4. For tax units without dependents, `net_investment_income` equals the all-member formula (person-level sources plus - `loss_limited_net_capital_gains`), so leaving dependents out changes - nothing for filers without dependents. + `loss_limited_net_capital_gains` plus the section 461(l) add-back), so + leaving dependents out changes nothing for filers without dependents. 5. 0 <= NIIT = 3.8% x min(max(0, NII), max(0, MAGI - threshold)). +6. 0 <= `net_investment_income_excess_business_loss` <= min(the section + 461(l) excess business loss, the net investment income losses), with + equality to the excess when those losses are the only business losses. + Section 1411(c)(1)(B) allows only deductions allowed by chapter 1. """ import numpy as np @@ -43,6 +49,7 @@ # income is drawn as a share of partnership_s_corp_income below. SIGNED_INPUTS = [ "rental_income", + "farm_rent_income", "estate_income", "short_term_capital_gains", "long_term_capital_gains", @@ -70,11 +77,14 @@ "qualified_dividend_income", "non_qualified_dividend_income", "rental_income", + "farm_rent_income", "passive_partnership_s_corp_income", "estate_income", ] OUTPUTS = [ "net_investment_income", + "excess_business_loss", + "net_investment_income_excess_business_loss", "net_investment_income_tax", "filer_loss_limited_net_capital_gains", "loss_limited_net_capital_gains", @@ -159,6 +169,24 @@ def _draw_units(): "head": {**zero, "long_term_capital_gains": 10_000.0}, "spouse": {**zero, "short_term_capital_gains": -4_000.0}, "dependents": [{**zero, "rental_income": 25_000.0}]}, + # Section 461(l) binds and the farm rental loss is the only loss. + {"kind": "single", "head_wages": 300_000.0, + "head": {**zero, "taxable_interest_income": 500_000.0, + "farm_rent_income": -600_000.0}, + "spouse": None, "dependents": []}, + # Binds on a joint return with a nonpassive partnership loss too. + {"kind": "joint", "head_wages": 400_000.0, + "head": {**zero, "taxable_interest_income": 300_000.0, + "rental_income": -500_000.0}, + "spouse": {**zero, "partnership_s_corp_income": -400_000.0, + "passive_partnership_s_corp_income": -200_000.0}, + "dependents": []}, + # A dependent's large loss would raise the filer's excess. + {"kind": "hoh", "head_wages": 300_000.0, + "head": {**zero, "taxable_interest_income": 400_000.0, + "rental_income": -400_000.0}, + "spouse": None, + "dependents": [{**zero, "farm_rent_income": -500_000.0}]}, ] # fmt: skip return units @@ -256,6 +284,46 @@ def _loss_limit(runs): return runs["irs"].capital_gains.loss_limit[runs["with"]["filing_status"]] +# The drawn inputs that section 461(l) aggregates as business items. +SECTION_461L_INPUTS = [ + "rental_income", + "farm_rent_income", + "estate_income", + "partnership_s_corp_income", +] + + +def _section_461l_reference(runs, units, include): + """Excess business loss and the part on NII losses, from leaf inputs. + + ``include`` selects the members counted (the head and spouse, or all). + The NII share is proportional: NII losses (rental and farm rental losses, + estate losses and the passive part of partnership losses) over all + business losses. + """ + amounts = {name: _by_person(units, name) for name in SECTION_461L_INPUTS} + income = _unit_sum(runs, include * sum(np.maximum(0, v) for v in amounts.values())) + loss = _unit_sum(runs, include * sum(np.maximum(0, -v) for v in amounts.values())) + passive_loss = np.minimum( + np.maximum(0, -_by_person(units, "passive_partnership_s_corp_income")), + np.maximum(0, -amounts["partnership_s_corp_income"]), + ) + investment_loss = _unit_sum( + runs, + include + * ( + np.maximum(0, -amounts["rental_income"]) + + np.maximum(0, -amounts["farm_rent_income"]) + + np.maximum(0, -amounts["estate_income"]) + + passive_loss + ), + ) + threshold = runs["irs"].ald.loss.max[runs["with"]["filing_status"]] + excess = np.maximum(0, loss - (income + threshold)) + share = np.divide(investment_loss, loss, out=np.zeros_like(loss), where=loss > 0) + return excess, excess * share, investment_loss, loss + + def test_dependent_flags_match_the_draw(runs, units): assert np.array_equal( runs["with"]["is_tax_unit_dependent"], _by_person(units, "is_dependent") @@ -291,11 +359,14 @@ def test_net_investment_income_matches_reference(runs, units): schedule_d = _unit_sum( runs, filer * sum(_by_person(units, name) for name in CAPITAL_INPUTS) ) - expected = _unit_sum(runs, filer * person_nii) + np.maximum( - -_loss_limit(runs), schedule_d + _, addback, _, _ = _section_461l_reference(runs, units, filer) + expected = ( + _unit_sum(runs, filer * person_nii) + + np.maximum(-_loss_limit(runs), schedule_d) + + addback ) np.testing.assert_allclose( - runs["with"]["net_investment_income"], expected, atol=TOLERANCE + runs["with"]["net_investment_income"], expected, rtol=1e-6, atol=TOLERANCE ) @@ -314,7 +385,13 @@ def test_filer_capital_gains_within_loss_limit(runs, units): def test_units_without_dependents_match_all_member_formula(runs, units): """Differential check against the all-member formula.""" person_nii = sum(_by_person(units, name) for name in NII_PERSON_INPUTS) - old = _unit_sum(runs, person_nii) + runs["with"]["loss_limited_net_capital_gains"] + everyone = np.ones_like(_by_person(units, "is_dependent"), dtype=bool) + _, addback, _, _ = _section_461l_reference(runs, units, everyone) + old = ( + _unit_sum(runs, person_nii) + + runs["with"]["loss_limited_net_capital_gains"] + + addback + ) no_dependents = np.array([not u["dependents"] for u in units]) np.testing.assert_allclose( runs["with"]["net_investment_income"][no_dependents], @@ -337,3 +414,29 @@ def test_niit_within_statutory_bounds(runs): # Both limbs of section 1411(a)(1) bind somewhere in the population. assert ((nii > 0) & (excess_magi > 0) & (nii < excess_magi)).sum() > 10 assert ((nii > 0) & (excess_magi > 0) & (nii > excess_magi)).sum() > 10 + + +def test_section_461l_addback_bounds(runs, units): + """Property 6, plus a differential check of both new variables.""" + filer = ~_by_person(units, "is_dependent") + excess, addback, investment_loss, loss = _section_461l_reference(runs, units, filer) + model_excess = runs["with"]["excess_business_loss"] + model_addback = runs["with"]["net_investment_income_excess_business_loss"] + np.testing.assert_allclose(model_excess, excess, rtol=1e-6, atol=TOLERANCE) + np.testing.assert_allclose(model_addback, addback, rtol=1e-6, atol=TOLERANCE) + assert np.all(model_addback >= -TOLERANCE) + assert np.all( + model_addback + <= np.minimum(model_excess, investment_loss) * (1 + 1e-6) + TOLERANCE + ) + only_investment_losses = (loss > 0) & np.isclose(investment_loss, loss) + np.testing.assert_allclose( + model_addback[only_investment_losses], + model_excess[only_investment_losses], + rtol=1e-6, + atol=TOLERANCE, + ) + # The edge rows make section 461(l) bind, including with a nonpassive + # partnership loss sharing the excess. + assert (model_excess > 0).sum() >= 3 + assert ((model_addback > 0) & (model_addback < model_excess - 1)).sum() >= 1 diff --git a/policyengine_us/tests/core/test_estate_income_invariants.py b/policyengine_us/tests/core/test_estate_income_invariants.py index d17c6959ef1..5110fa3b0ab 100644 --- a/policyengine_us/tests/core/test_estate_income_invariants.py +++ b/policyengine_us/tests/core/test_estate_income_invariants.py @@ -14,7 +14,11 @@ income is excluded from the filer's gross income throughout the model). 2. Tax unit net investment income rises by exactly the sum of estate_income plus the Schedule K-1 (Form 1041) box 14 code H adjustment over non-dependent - members (Form 8960 lines 4a and 7). + members (Form 8960 lines 4a and 7), less the part of an estate loss that + section 461(l) disallows: § 1411(c)(1)(B) allows only deductions allowed by + chapter 1. Estate income is the only business item drawn here, so the whole + excess business loss falls on estate losses that net investment income + counts. 3. When every member's estate income is nonnegative and nothing else produces a loss, AGI rises by exactly the non-dependents' estate income. 4. A dependent's estate income and adjustment, of either sign, leave the @@ -238,9 +242,11 @@ def runs(request, units): ) out[name]["filing_status"] = sim.calculate("filing_status", year) out[name]["person_tax_unit"] = sim.populations["tax_unit"].members_entity_id - out["niit_parameters"] = sim.tax_benefit_system.parameters( - f"{year}-01-01" - ).gov.irs.investment.net_investment_income_tax + year_parameters = sim.tax_benefit_system.parameters(f"{year}-01-01") + out["niit_parameters"] = ( + year_parameters.gov.irs.investment.net_investment_income_tax + ) + out["section_461l_threshold"] = year_parameters.gov.irs.ald.loss.max return out @@ -269,12 +275,35 @@ def test_gross_income_includes_positive_estate_income_of_non_dependents(runs, un np.testing.assert_allclose(delta, expected, atol=TOLERANCE) +def _section_461l_estate_addback(runs, units): + """The estate losses section 461(l) disallows that NII would otherwise take. + + Estate income is the only business item drawn, so the excess business loss + is the head and spouse's estate losses beyond their estate income plus the + threshold, and all of it falls on net investment income losses: a Part III + loss stays on Form 8960 line 4a whatever the code H adjustment (line 7). + """ + estate, _, dependent = _person_inputs(units) + filer = ~dependent + tax_unit = runs["with"]["person_tax_unit"] + + def by_unit(values): + return np.bincount(tax_unit, weights=filer * values, minlength=len(units)) + + income = by_unit(np.maximum(0, estate)) + loss = by_unit(np.maximum(0, -estate)) + threshold = runs["section_461l_threshold"][runs["with"]["filing_status"]] + return np.maximum(0, loss - (income + threshold)) + + def test_net_investment_income_adds_estate_income_and_code_h(runs, units): estate, adjustment, dependent = _person_inputs(units) per_person = np.where(dependent, 0, estate + adjustment) expected = np.bincount( runs["with"]["person_tax_unit"], weights=per_person, minlength=len(units) - ) + ) + _section_461l_estate_addback(runs, units) + # The two edge rows with a 400,000 estate loss make section 461(l) bind. + assert (_section_461l_estate_addback(runs, units) > 0).sum() >= 2 delta = ( runs["with"]["net_investment_income"] - runs["zero"]["net_investment_income"] ) diff --git a/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/excess_business_loss.yaml b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/excess_business_loss.yaml new file mode 100644 index 00000000000..9006a281c36 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/excess_business_loss.yaml @@ -0,0 +1,115 @@ +# Section 461(l): business losses beyond business income plus the threshold +# are disallowed this year (Form 461 line 16). 2025 thresholds: 313,000, or +# 626,000 on a joint return. + +- name: A loss within the threshold leaves no excess + period: 2025 + input: + people: + person1: + age: 50 + is_tax_unit_head: true + self_employment_income: -100_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + limited_business_loss: 100_000 + excess_business_loss: 0 + +- name: A Form 4835 loss beyond the threshold + # Allowed 313,000 of the 600,000 loss; 287,000 disallowed. + period: 2025 + input: + people: + person1: + age: 60 + is_tax_unit_head: true + farm_rent_income: -600_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + limited_business_loss: 313_000 + excess_business_loss: 287_000 + +- name: Business income raises the limit + # Limit 100,000 + 313,000 = 413,000; 600,000 - 413,000 = 187,000. + period: 2025 + input: + people: + person1: + age: 60 + is_tax_unit_head: true + rental_income: 100_000 + farm_rent_income: -600_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + limited_business_loss: 413_000 + excess_business_loss: 187_000 + +- name: A joint return combines both spouses' losses + # 400,000 + 400,000 = 800,000; 800,000 - 626,000 = 174,000. + period: 2025 + input: + people: + head: + age: 60 + is_tax_unit_head: true + self_employment_income: -400_000 + spouse: + age: 58 + is_tax_unit_spouse: true + farm_rent_income: -400_000 + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: TX + output: + filing_status: JOINT + limited_business_loss: 626_000 + excess_business_loss: 174_000 + +- name: A dependent's loss stays off the filer's Form 461 + # Only the parent's 400,000; 400,000 - 313,000 = 87,000. + period: 2025 + input: + people: + parent: + age: 50 + is_tax_unit_head: true + self_employment_income: -400_000 + child: + age: 16 + is_tax_unit_dependent: true + farm_rent_income: -400_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: TX + output: + limited_business_loss: 313_000 + excess_business_loss: 87_000 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/ira/policybench_ira.yaml b/policyengine_us/tests/policy/baseline/gov/irs/ira/policybench_ira.yaml index dc32eb9c434..6308ba77e0c 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/ira/policybench_ira.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/ira/policybench_ira.yaml @@ -683,5 +683,9 @@ output: tax_units: tax_unit: - income_tax_before_refundable_credits: 40423.17578125 + # The 2.37.2 evidence value was 40423.17578125. Net investment income + # now includes the -2,446.59 Form 4835 farm rental loss (Schedule E + # line 40), as AGI already did: NII 4,300 -> 1,853.41 with MAGI + # 234,414.20, so NIIT falls by 3.8% x 2,446.59 = 92.97. + income_tax_before_refundable_credits: 40330.20703125 state_income_tax_before_refundable_credits: 11919.0576171875 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_excess_business_loss.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_excess_business_loss.yaml new file mode 100644 index 00000000000..c76685ff1c7 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_excess_business_loss.yaml @@ -0,0 +1,147 @@ +# The part of the section 461(l) excess business loss (2025 single threshold +# 313,000) that falls on losses net investment income counts. Net investment +# income adds it back (26 USC 1411(c)(1)(B): deductions allowed by chapter 1). + +- name: All of the excess when net investment income losses are the only losses + # Form 4835 loss 600,000; excess 600,000 - 313,000 = 287,000, all of it on + # the farm rental loss. + period: 2025 + input: + people: + person1: + age: 60 + is_tax_unit_head: true + farm_rent_income: -600_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + excess_business_loss: 287_000 + net_investment_income_excess_business_loss: 287_000 + +- name: None of the excess when no loss is a net investment income loss + period: 2025 + input: + people: + person1: + age: 50 + is_tax_unit_head: true + self_employment_income: -600_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + excess_business_loss: 287_000 + net_investment_income_excess_business_loss: 0 + +- name: A proportional share when both kinds of loss are present + # Schedule C loss 300,000 and Form 4835 loss 300,000; excess 287,000. + # Provisional proportional share: 287,000 x 300,000 / 600,000 = 143,500. + # (The law does not say how to split it; see the variable documentation.) + period: 2025 + input: + people: + person1: + age: 60 + is_tax_unit_head: true + self_employment_income: -300_000 + farm_rent_income: -300_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + excess_business_loss: 287_000 + net_investment_income_excess_business_loss: 143_500 + +- name: Only the passive part of a partnership loss counts + # Partnership/S-corp loss 400,000, of which 100,000 is passive; excess + # 400,000 - 313,000 = 87,000; share 87,000 x 100,000 / 400,000 = 21,750. + period: 2025 + input: + people: + person1: + age: 60 + is_tax_unit_head: true + partnership_s_corp_income: -400_000 + passive_partnership_s_corp_income: -100_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + excess_business_loss: 87_000 + net_investment_income_excess_business_loss: 21_750 + +- name: A positive code H adjustment does not take an estate loss out of net investment income + # Schedule E Part III loss 600,000 (Form 8960 line 4a) and a +600,000 + # Schedule K-1 (Form 1041) box 14 code H adjustment (line 7). Code H is + # positive only for section 1291 fund, CFC and QEF distributions that are + # NII but not taxable income (2025 Form 1041 instructions, Beneficiary + # reporting), so it is separate income and the whole estate loss is still + # a net investment income loss. Excess 600,000 - 313,000 = 287,000. + period: 2025 + input: + people: + person1: + age: 60 + is_tax_unit_head: true + estate_income: -600_000 + estate_income_net_investment_income_adjustment: 600_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + excess_business_loss: 287_000 + net_investment_income_excess_business_loss: 287_000 + +- name: A joint return shares the excess across both spouses' losses + # Head Schedule E rental loss 500,000; spouse partnership loss 400,000, of + # which 200,000 is passive. Losses 900,000; excess 900,000 - 626,000 = + # 274,000; net investment income losses 500,000 + 200,000 = 700,000; share + # 274,000 x 700,000 / 900,000 = 213,111.11. + period: 2025 + absolute_error_margin: 0.1 + input: + people: + head: + age: 60 + is_tax_unit_head: true + rental_income: -500_000 + spouse: + age: 58 + is_tax_unit_spouse: true + partnership_s_corp_income: -400_000 + passive_partnership_s_corp_income: -200_000 + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: TX + output: + filing_status: JOINT + excess_business_loss: 274_000 + net_investment_income_excess_business_loss: 213_111.11 diff --git a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_tax.yaml b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_tax.yaml index d4983683baa..23a25297ea8 100644 --- a/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_tax.yaml +++ b/policyengine_us/tests/policy/baseline/gov/irs/tax/federal_income/net_investment_income_tax.yaml @@ -488,3 +488,229 @@ # The default (the full 100,000) would give 1,900. niit_magi: 220_000 net_investment_income_tax: 760 + +- name: Farm rental income counts as net investment income + # 2025 Form 4835 line 32 net farm rental income of 30,000 goes on Schedule + # E line 40, then into line 41 and Schedule 1 line 5. Form 8960 line 4a + # takes Schedule 1 line 5. Form 4835 is only for a rental activity under the + # passive activity rules, so line 4b removes nothing. Line 12 NII 30,000; + # line 13 MAGI 190,000 + 30,000 = 220,000; line 15 220,000 - 200,000 = + # 20,000; line 16 min(30,000, 20,000) = 20,000; line 17 3.8% x 20,000 = 760. + period: 2025 + input: + people: + person1: + age: 60 + is_tax_unit_head: true + employment_income: 190_000 + farm_rent_income: 30_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + adjusted_gross_income: 220_000 + net_investment_income: 30_000 + net_investment_income_tax: 760 + +- name: A farm rental loss nets against other rents on Schedule E line 41 + # Schedule E line 26 rents of 40,000 and a Form 4835 line 34c loss of + # 10,000 (allowed in full against the passive rental income) give line 41 + # and Schedule 1 line 5 of 30,000, so Form 8960 line 4a is 30,000. AGI and + # MAGI 250,000 + 30,000 = 280,000; line 15 80,000; line 16 min(30,000, + # 80,000) = 30,000; line 17 3.8% x 30,000 = 1,140. Leaving the loss out + # would give 3.8% x 40,000 = 1,520. + period: 2025 + input: + people: + person1: + age: 60 + is_tax_unit_head: true + employment_income: 250_000 + rental_income: 40_000 + farm_rent_income: -10_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + adjusted_gross_income: 280_000 + net_investment_income: 30_000 + net_investment_income_tax: 1_140 + +- name: A spouse's farm rental income counts on a joint return + # 2024 Form 8960 line 4a also takes Schedule 1 line 5. Line 12 NII 35,000; + # line 13 MAGI 240,000 + 35,000 = 275,000; line 14 joint threshold + # 250,000; line 15 25,000; line 16 min(35,000, 25,000) = 25,000; line 17 + # 3.8% x 25,000 = 950. + period: 2024 + input: + people: + head: + age: 60 + is_tax_unit_head: true + employment_income: 240_000 + spouse: + age: 58 + is_tax_unit_spouse: true + farm_rent_income: 35_000 + tax_units: + tax_unit: + members: [head, spouse] + marital_units: + marital_unit: + members: [head, spouse] + households: + household: + members: [head, spouse] + state_code: TX + output: + filing_status: JOINT + adjusted_gross_income: 275_000 + net_investment_income: 35_000 + net_investment_income_tax: 950 + +- name: A dependent's farm rental income stays off the filer's Form 8960 + # The child's Form 4835 income goes on the child's own return, so the + # parent's line 12 NII is the 5,000 of interest. MAGI 210,000 + 5,000 = + # 215,000; line 15 15,000; line 17 3.8% x min(5,000, 15,000) = 190. + # Counting the child's rent would give 3.8% x 15,000 = 570. + period: 2025 + input: + people: + parent: + age: 50 + is_tax_unit_head: true + employment_income: 210_000 + taxable_interest_income: 5_000 + child: + age: 16 + is_tax_unit_dependent: true + farm_rent_income: 10_000 + tax_units: + tax_unit: + members: [parent, child] + households: + household: + members: [parent, child] + state_code: TX + output: + irs_gross_income: [215_000, 0] + adjusted_gross_income: 215_000 + net_investment_income: 5_000 + net_investment_income_tax: 190 + +- name: A Form 4835 loss reduces net investment income only as far as section 461(l) allows + # Independent review counterexample. Wages 300,000, interest 500,000, Form + # 4835 loss 600,000 (allowed under the at-risk and passive activity rules). + # Section 461(l) allows 313,000 and disallows 287,000. AGI and MAGI + # 300,000 + 500,000 - 313,000 = 487,000. NII 500,000 - 313,000 = 187,000. + # NIIT 3.8% x min(187,000, 487,000 - 200,000) = 7,106. Counting the full + # loss would give NII -100,000 and no tax. + period: 2025 + input: + people: + person1: + age: 60 + is_tax_unit_head: true + employment_income: 300_000 + taxable_interest_income: 500_000 + farm_rent_income: -600_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + adjusted_gross_income: 487_000 + net_investment_income: 187_000 + net_investment_income_tax: 7_106 + +- name: A Schedule E rental loss is limited the same way + period: 2025 + input: + people: + person1: + age: 60 + is_tax_unit_head: true + employment_income: 300_000 + taxable_interest_income: 500_000 + rental_income: -600_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + adjusted_gross_income: 487_000 + net_investment_income: 187_000 + net_investment_income_tax: 7_106 + +- name: A Schedule C loss takes a proportional share of the section 461(l) excess + # Wages 600,000, interest 200,000, Schedule C loss 300,000, Form 4835 loss + # 300,000. Section 461(l) allows 313,000 of the 600,000; AGI and MAGI + # 800,000 - 313,000 = 487,000. Provisional proportional share of the + # 287,000 excess on the farm rental loss: 143,500, so NII 200,000 - 300,000 + # + 143,500 = 43,500. NIIT 3.8% x min(43,500, 287,000) = 1,653. (No rule + # says how to split the excess; see net_investment_income_excess_business_loss.) + period: 2025 + input: + people: + person1: + age: 60 + is_tax_unit_head: true + employment_income: 600_000 + taxable_interest_income: 200_000 + self_employment_income: -300_000 + farm_rent_income: -300_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + adjusted_gross_income: 487_000 + net_investment_income: 43_500 + net_investment_income_tax: 1_653 + +- name: A positive code H adjustment leaves the estate loss limited by section 461(l) + # Independent review counterexample. Wages 300,000, interest 500,000, + # Schedule E Part III loss 600,000, and +600,000 code H from a trust's + # NII-only CFC distribution (positive code H can be nothing else). Section + # 461(l) allows 313,000 of the loss: AGI 300,000 + 500,000 - 313,000 = + # 487,000. MAGI adds the positive code H: 1,087,000. NII 500,000 - 313,000 + # + 600,000 = 787,000. NIIT 3.8% x min(787,000, 887,000) = 29,906. + period: 2025 + input: + people: + person1: + age: 60 + is_tax_unit_head: true + employment_income: 300_000 + taxable_interest_income: 500_000 + estate_income: -600_000 + estate_income_net_investment_income_adjustment: 600_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: TX + output: + adjusted_gross_income: 487_000 + niit_magi: 1_087_000 + net_investment_income: 787_000 + net_investment_income_tax: 29_906 diff --git a/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/mn_niit.yaml b/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/mn_niit.yaml index 277a05bfac5..c0ae2eb6c5c 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/mn_niit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mn/tax/income/mn_niit.yaml @@ -15,3 +15,27 @@ state_code: MN output: mn_niit: 1_000 # (1_100_000 - 1_000_000) * 0.01 + +- name: Minnesota net investment income includes farm rental income + # Minn. Stat. 290.033(a) uses the section 1411(c) definition, less gain on + # class 2a property, so Form 4835 rent counts. NII 800,000 of interest + + # 300,000 of farm rent = 1,100,000; 1% x (1,100,000 - 1,000,000) = 1,000. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 60 + is_tax_unit_head: true + taxable_interest_income: 800_000 + farm_rent_income: 300_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_code: MN + output: + net_investment_income: 1_100_000 + mn_niit: 1_000 diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/excess_business_loss.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/excess_business_loss.py new file mode 100644 index 00000000000..bf3a82ac197 --- /dev/null +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/excess_business_loss.py @@ -0,0 +1,26 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.irs.income.taxable_income.adjusted_gross_income.above_the_line_deductions.limited_business_loss import ( + section_461l_business_income_and_loss, +) + + +class excess_business_loss(Variable): + value_type = float + entity = TaxUnit + label = "Excess business loss disallowed under section 461(l)" + unit = USD + documentation = ( + "The head and spouse's business losses beyond business income plus " + "the section 461(l) threshold (Form 461 line 16): the part of their " + "business losses that limited_business_loss does not deduct this " + "year." + ) + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/461#l", + "https://www.irs.gov/pub/irs-prior/i461--2025.pdf#page=3", + ) + + def formula(tax_unit, period, parameters): + _, business_loss = section_461l_business_income_and_loss(tax_unit, period) + return max_(0, business_loss - tax_unit("limited_business_loss", period)) diff --git a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/limited_business_loss.py b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/limited_business_loss.py index 153c74d4982..d48a0a7a485 100644 --- a/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/limited_business_loss.py +++ b/policyengine_us/variables/gov/irs/income/taxable_income/adjusted_gross_income/above_the_line_deductions/limited_business_loss.py @@ -1,5 +1,45 @@ from policyengine_us.model_api import * +# Person-level business items that section 461(l) aggregates, in the order +# they are summed: Schedule C, Schedule F, Schedule E rental, Form 4835 farm +# rent, Schedule E estate/trust and partnership/S-corp income. A positive +# amount is business income and a negative amount a business loss. +SECTION_461L_PERSON_ITEMS = [ + "total_self_employment_income", + "farm_operations_income", + "rental_income", + "farm_rent_income", + "estate_income", + "partnership_s_corp_income", +] + + +def section_461l_business_income_and_loss(tax_unit, period): + """ + The head and spouse's business income and business losses under 461(l). + + Returns the two aggregates section 461(l)(3)(A) compares: gross business + income and gains, and business deductions, each summed over the items in + SECTION_461L_PERSON_ITEMS and Form 4797 other net gain. A tax-unit + dependent's business, rental and estate items belong on the dependent's + own return, as in irs_gross_income. They neither add to this return's + losses nor raise its section 461(l) limit. + """ + person = tax_unit.members + not_dependent = ~person("is_tax_unit_dependent", period) + business_income = 0 + business_loss = 0 + for item in SECTION_461L_PERSON_ITEMS: + amount = person(item, period) + business_income = business_income + tax_unit.sum( + not_dependent * max_(0, amount) + ) + business_loss = business_loss + tax_unit.sum(not_dependent * max_(0, -amount)) + other_net_gain = tax_unit("other_net_gain", period) + business_income = business_income + max_(0, other_net_gain) + business_loss = business_loss + max_(0, -other_net_gain) + return business_income, business_loss + class limited_business_loss(Variable): value_type = float @@ -22,75 +62,12 @@ def formula(tax_unit, period, parameters): filing_status = tax_unit("filing_status", period) # Section 461(l) excess business loss limitation threshold_amount = parameters(period).gov.irs.ald.loss.max[filing_status] - person = tax_unit.members - # A tax-unit dependent's business, rental and estate items belong on - # the dependent's own return, as in irs_gross_income. They neither - # add to this return's losses nor raise its Section 461(l) limit. - not_dependent = ~person("is_tax_unit_dependent", period) - # Section 461(l) compares business deductions to business gross # income/gains plus the threshold amount. In this model, positive # business amounts flow into gross income and losses are deducted here, # so allowable business losses are capped at business income plus # the threshold. - indiv_se_income = max_(0, person("total_self_employment_income", period)) - indiv_se_loss = max_(0, -person("total_self_employment_income", period)) - self_employment_income = tax_unit.sum(not_dependent * indiv_se_income) - self_employment_loss = tax_unit.sum(not_dependent * indiv_se_loss) - - # Schedule F farm business income/losses. - indiv_farm_operations_income = max_(0, person("farm_operations_income", period)) - indiv_farm_loss = max_(0, -person("farm_operations_income", period)) - farm_operations_income = tax_unit.sum( - not_dependent * indiv_farm_operations_income - ) - farm_loss = tax_unit.sum(not_dependent * indiv_farm_loss) - - # Schedule E rental, farm-rent, and estate/trust items. - indiv_rental_income = max_(0, person("rental_income", period)) - indiv_rental_loss = max_(0, -person("rental_income", period)) - rental_income = tax_unit.sum(not_dependent * indiv_rental_income) - rental_loss = tax_unit.sum(not_dependent * indiv_rental_loss) - - indiv_farm_rent_income = max_(0, person("farm_rent_income", period)) - indiv_farm_rent_loss = max_(0, -person("farm_rent_income", period)) - farm_rent_income = tax_unit.sum(not_dependent * indiv_farm_rent_income) - farm_rent_loss = tax_unit.sum(not_dependent * indiv_farm_rent_loss) - - indiv_estate_income = max_(0, person("estate_income", period)) - indiv_estate_loss = max_(0, -person("estate_income", period)) - estate_income = tax_unit.sum(not_dependent * indiv_estate_income) - estate_loss = tax_unit.sum(not_dependent * indiv_estate_loss) - - # Partnership/S-corp losses (Schedule E) - indiv_scorp_income = max_(0, person("partnership_s_corp_income", period)) - indiv_scorp_loss = max_(0, -person("partnership_s_corp_income", period)) - partnership_s_corp_income = tax_unit.sum(not_dependent * indiv_scorp_income) - partnership_s_corp_loss = tax_unit.sum(not_dependent * indiv_scorp_loss) - - other_net_gain = tax_unit("other_net_gain", period) - other_business_gain = max_(0, other_net_gain) - other_business_loss = max_(0, -other_net_gain) - - # Total business losses subject to Section 461(l) limitation - total_business_income = ( - self_employment_income - + farm_operations_income - + rental_income - + farm_rent_income - + estate_income - + partnership_s_corp_income - + other_business_gain - ) - total_business_loss = ( - self_employment_loss - + farm_loss - + rental_loss - + farm_rent_loss - + estate_loss - + partnership_s_corp_loss - + other_business_loss + business_income, business_loss = section_461l_business_income_and_loss( + tax_unit, period ) - max_deductible_business_loss = total_business_income + threshold_amount - limited_business_loss = min_(total_business_loss, max_deductible_business_loss) - return limited_business_loss + return min_(business_loss, business_income + threshold_amount) diff --git a/policyengine_us/variables/gov/irs/tax/federal_income/net_investment_income_excess_business_loss.py b/policyengine_us/variables/gov/irs/tax/federal_income/net_investment_income_excess_business_loss.py new file mode 100644 index 00000000000..6bdc110d0a9 --- /dev/null +++ b/policyengine_us/variables/gov/irs/tax/federal_income/net_investment_income_excess_business_loss.py @@ -0,0 +1,60 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.irs.income.taxable_income.adjusted_gross_income.above_the_line_deductions.limited_business_loss import ( + section_461l_business_income_and_loss, +) + + +class net_investment_income_excess_business_loss(Variable): + value_type = float + entity = TaxUnit + label = "Excess business loss attributed to net investment income" + unit = USD + documentation = ( + "The part of the head and spouse's section 461(l) excess business " + "loss that falls on losses net investment income counts: Schedule E " + "rental, Form 4835 farm rental and Schedule E Part III estate and " + "trust losses, and the passive part of partnership/S corporation " + "losses. Section 1411(c)(1)(B) allows only the deductions allowed by " + "chapter 1, so net investment income adds this back. A Schedule K-1 " + "(Form 1041) box 14 code H adjustment is a separate Form 8960 line 7 " + "item and does not change how much of an estate or trust loss is a " + "net investment income loss. When the head and spouse also have " + "business losses net investment income does not count, the excess is " + "provisionally shared in proportion to the two kinds of loss: no " + "statute, regulation or form instruction says how to split it." + ) + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/uscode/text/26/1411#c_1_B", + "https://www.law.cornell.edu/uscode/text/26/461#l", + "https://www.ecfr.gov/current/title-26/section-1.1411-4#p-1.1411-4(f)", + # Beneficiary reporting: code H is positive only for section 1291 fund, + # CFC and QEF distributions that are NII but not taxable income. + "https://www.irs.gov/pub/irs-prior/i1041--2025.pdf#page=38", + ) + + def formula(tax_unit, period, parameters): + person = tax_unit.members + not_dependent = ~person("is_tax_unit_dependent", period) + rental_loss = max_(0, -person("rental_income", period)) + farm_rent_loss = max_(0, -person("farm_rent_income", period)) + estate_loss = max_(0, -person("estate_income", period)) + # Only the passive part of a partnership/S corporation loss is a net + # investment income loss, and no more than the loss section 461(l) + # counts. + passive_loss = min_( + max_(0, -person("passive_partnership_s_corp_income", period)), + max_(0, -person("partnership_s_corp_income", period)), + ) + investment_loss = tax_unit.sum( + not_dependent * (rental_loss + farm_rent_loss + estate_loss + passive_loss) + ) + _, business_loss = section_461l_business_income_and_loss(tax_unit, period) + # Provisional proportional split; see the documentation. + investment_share = np.divide( + investment_loss, + business_loss, + out=np.zeros_like(investment_loss), + where=business_loss > 0, + ) + return tax_unit("excess_business_loss", period) * investment_share diff --git a/policyengine_us/variables/household/income/person/farm/farm_rent_income.py b/policyengine_us/variables/household/income/person/farm/farm_rent_income.py index dd5d723a63d..b2d9522b40b 100644 --- a/policyengine_us/variables/household/income/person/farm/farm_rent_income.py +++ b/policyengine_us/variables/household/income/person/farm/farm_rent_income.py @@ -5,7 +5,16 @@ class farm_rent_income(Variable): value_type = float entity = Person label = "farm rental income" + documentation = ( + "Net farm rental income or loss from Form 4835: crop or livestock " + "shares a landowner (or sub-lessor) receives from a farm in which " + "they do not materially participate. It is reported on Schedule E " + "line 40 and is not subject to self-employment tax." + ) unit = USD definition_period = YEAR - reference = "https://www.law.cornell.edu/uscode/text/26/1402" + reference = ( + "https://www.law.cornell.edu/uscode/text/26/1402", + "https://www.irs.gov/pub/irs-prior/f4835--2025.pdf#page=2", + ) uprating = "calibration.gov.irs.soi.farm_rent_income"