From c5a77ca83f029bf62f9a051f0570c51cf43c584b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 02:37:56 -0400 Subject: [PATCH 1/9] Add independent S.770 section 8 tax and OASDI transfer options --- ...n8-investment-business-income-tax.added.md | 1 + .../section_8/active_business/deny_nol.yaml | 12 + .../section_8/active_business/in_effect.yaml | 11 + .../active_business/oasdi_share.yaml | 11 + .../s770/section_8/active_business/rate.yaml | 10 + .../section_8/active_business/start_year.yaml | 11 + .../s770/section_8/nii_addon/in_effect.yaml | 11 + .../s770/section_8/nii_addon/oasdi_share.yaml | 12 + .../s770/section_8/nii_addon/rate.yaml | 11 + .../s770/section_8/nii_addon/start_year.yaml | 11 + .../s770/section_8/transfer/bill_share.yaml | 11 + .../s770/section_8/transfer/mode.yaml | 13 + .../s770/section_8/transfer/oasdi_rate.yaml | 12 + .../s770/section_8/transfer/start_year.yaml | 12 + .../reforms/congress/s770/README.md | 143 ++++ .../reforms/congress/s770/__init__.py | 1 + .../reforms/congress/s770/section_8.py | 122 +++ policyengine_us/reforms/reforms.py | 4 + .../contrib/congress/s770/section_8.yaml | 698 ++++++++++++++++++ .../tests/reforms/test_s770_section_8.py | 389 ++++++++++ .../s770/s770_active_business_income.py | 50 ++ ...70_active_business_income_revenue_oasdi.py | 21 + .../s770/s770_active_business_income_tax.py | 22 + .../congress/s770/s770_active_fica_income.py | 11 + .../congress/s770/s770_active_taxable_base.py | 32 + .../contrib/congress/s770/s770_draw_gross.py | 11 + .../congress/s770/s770_draw_hi_in_effect.py | 10 + .../congress/s770/s770_draw_oasdi_base.py | 11 + .../s770/s770_existing_niit_revenue_oasdi.py | 21 + .../congress/s770/s770_existing_niit_tax.py | 20 + .../s770/s770_nii_addon_revenue_oasdi.py | 21 + .../congress/s770/s770_nii_addon_tax.py | 22 + .../contrib/congress/s770/s770_nii_base.py | 33 + .../s770/s770_nii_fica_seca_income.py | 11 + .../congress/s770/s770_nii_taxable_base.py | 31 + .../congress/s770/s770_niit_nol_deduction.py | 11 + .../congress/s770/s770_passive_se_income.py | 11 + .../congress/s770/s770_revenue_oasdi.py | 16 + 38 files changed, 1871 insertions(+) create mode 100644 changelog.d/reform-s770-section8-investment-business-income-tax.added.md create mode 100644 policyengine_us/parameters/gov/contrib/congress/s770/section_8/active_business/deny_nol.yaml create mode 100644 policyengine_us/parameters/gov/contrib/congress/s770/section_8/active_business/in_effect.yaml create mode 100644 policyengine_us/parameters/gov/contrib/congress/s770/section_8/active_business/oasdi_share.yaml create mode 100644 policyengine_us/parameters/gov/contrib/congress/s770/section_8/active_business/rate.yaml create mode 100644 policyengine_us/parameters/gov/contrib/congress/s770/section_8/active_business/start_year.yaml create mode 100644 policyengine_us/parameters/gov/contrib/congress/s770/section_8/nii_addon/in_effect.yaml create mode 100644 policyengine_us/parameters/gov/contrib/congress/s770/section_8/nii_addon/oasdi_share.yaml create mode 100644 policyengine_us/parameters/gov/contrib/congress/s770/section_8/nii_addon/rate.yaml create mode 100644 policyengine_us/parameters/gov/contrib/congress/s770/section_8/nii_addon/start_year.yaml create mode 100644 policyengine_us/parameters/gov/contrib/congress/s770/section_8/transfer/bill_share.yaml create mode 100644 policyengine_us/parameters/gov/contrib/congress/s770/section_8/transfer/mode.yaml create mode 100644 policyengine_us/parameters/gov/contrib/congress/s770/section_8/transfer/oasdi_rate.yaml create mode 100644 policyengine_us/parameters/gov/contrib/congress/s770/section_8/transfer/start_year.yaml create mode 100644 policyengine_us/reforms/congress/s770/README.md create mode 100644 policyengine_us/reforms/congress/s770/__init__.py create mode 100644 policyengine_us/reforms/congress/s770/section_8.py create mode 100644 policyengine_us/tests/policy/contrib/congress/s770/section_8.yaml create mode 100644 policyengine_us/tests/reforms/test_s770_section_8.py create mode 100644 policyengine_us/variables/gov/contrib/congress/s770/s770_active_business_income.py create mode 100644 policyengine_us/variables/gov/contrib/congress/s770/s770_active_business_income_revenue_oasdi.py create mode 100644 policyengine_us/variables/gov/contrib/congress/s770/s770_active_business_income_tax.py create mode 100644 policyengine_us/variables/gov/contrib/congress/s770/s770_active_fica_income.py create mode 100644 policyengine_us/variables/gov/contrib/congress/s770/s770_active_taxable_base.py create mode 100644 policyengine_us/variables/gov/contrib/congress/s770/s770_draw_gross.py create mode 100644 policyengine_us/variables/gov/contrib/congress/s770/s770_draw_hi_in_effect.py create mode 100644 policyengine_us/variables/gov/contrib/congress/s770/s770_draw_oasdi_base.py create mode 100644 policyengine_us/variables/gov/contrib/congress/s770/s770_existing_niit_revenue_oasdi.py create mode 100644 policyengine_us/variables/gov/contrib/congress/s770/s770_existing_niit_tax.py create mode 100644 policyengine_us/variables/gov/contrib/congress/s770/s770_nii_addon_revenue_oasdi.py create mode 100644 policyengine_us/variables/gov/contrib/congress/s770/s770_nii_addon_tax.py create mode 100644 policyengine_us/variables/gov/contrib/congress/s770/s770_nii_base.py create mode 100644 policyengine_us/variables/gov/contrib/congress/s770/s770_nii_fica_seca_income.py create mode 100644 policyengine_us/variables/gov/contrib/congress/s770/s770_nii_taxable_base.py create mode 100644 policyengine_us/variables/gov/contrib/congress/s770/s770_niit_nol_deduction.py create mode 100644 policyengine_us/variables/gov/contrib/congress/s770/s770_passive_se_income.py create mode 100644 policyengine_us/variables/gov/contrib/congress/s770/s770_revenue_oasdi.py diff --git a/changelog.d/reform-s770-section8-investment-business-income-tax.added.md b/changelog.d/reform-s770-section8-investment-business-income-tax.added.md new file mode 100644 index 00000000000..09d245985e8 --- /dev/null +++ b/changelog.d/reform-s770-section8-investment-business-income-tax.added.md @@ -0,0 +1 @@ +Add independently enabled S.770 section 8 NII add-on and active pass-through income tax options, signed unified-base allocation, and configurable OASDI revenue transfers. diff --git a/policyengine_us/parameters/gov/contrib/congress/s770/section_8/active_business/deny_nol.yaml b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/active_business/deny_nol.yaml new file mode 100644 index 00000000000..bd0226b7068 --- /dev/null +++ b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/active_business/deny_nol.yaml @@ -0,0 +1,12 @@ +description: When the active option is enabled, add back the validated NOL deduction supplied + in s770_niit_nol_deduction to broadened NII before allocating the unified base. There + is no automatic federal section 172 deduction variable. +values: + '0000-01-01': true +metadata: + label: S.770 broadened NII denies NOL deduction + period: year + reference: + - title: S.770 (119th Congress), sections 8 and 9 + href: https://www.congress.gov/bill/119th-congress/senate-bill/770/text + unit: bool diff --git a/policyengine_us/parameters/gov/contrib/congress/s770/section_8/active_business/in_effect.yaml b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/active_business/in_effect.yaml new file mode 100644 index 00000000000..d401534a47f --- /dev/null +++ b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/active_business/in_effect.yaml @@ -0,0 +1,11 @@ +description: The signed additional active-business base applies if this is true, independently + of the NII add-on. +values: + '0000-01-01': false +metadata: + label: S.770 active business option in effect + period: year + reference: + - title: S.770 (119th Congress), sections 8 and 9 + href: https://www.congress.gov/bill/119th-congress/senate-bill/770/text + unit: bool diff --git a/policyengine_us/parameters/gov/contrib/congress/s770/section_8/active_business/oasdi_share.yaml b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/active_business/oasdi_share.yaml new file mode 100644 index 00000000000..8ec6b13ab7a --- /dev/null +++ b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/active_business/oasdi_share.yaml @@ -0,0 +1,11 @@ +description: In custom and all_addon transfer modes, this share of the active business + tax goes to OASDI. +values: + '0000-01-01': 0.62 +metadata: + label: S.770 active business OASDI share + period: year + reference: + - title: S.770 (119th Congress), sections 8 and 9 + href: https://www.congress.gov/bill/119th-congress/senate-bill/770/text + unit: /1 diff --git a/policyengine_us/parameters/gov/contrib/congress/s770/section_8/active_business/rate.yaml b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/active_business/rate.yaml new file mode 100644 index 00000000000..11b1f8fcd7b --- /dev/null +++ b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/active_business/rate.yaml @@ -0,0 +1,10 @@ +description: The active business option taxes its allocated base at this total rate. +values: + '0000-01-01': 0.162 +metadata: + label: S.770 active business tax rate + period: year + reference: + - title: S.770 (119th Congress), sections 8 and 9 + href: https://www.congress.gov/bill/119th-congress/senate-bill/770/text + unit: /1 diff --git a/policyengine_us/parameters/gov/contrib/congress/s770/section_8/active_business/start_year.yaml b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/active_business/start_year.yaml new file mode 100644 index 00000000000..224fa59bbc7 --- /dev/null +++ b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/active_business/start_year.yaml @@ -0,0 +1,11 @@ +description: The active-business option starts in this tax year. Section 8(c) specifies + taxable years beginning after enactment; 2026 is an illustrative enactment assumption. +values: + '0000-01-01': 2026 +metadata: + label: S.770 active business start year + period: year + reference: + - title: S.770 (119th Congress), sections 8 and 9 + href: https://www.congress.gov/bill/119th-congress/senate-bill/770/text + unit: year diff --git a/policyengine_us/parameters/gov/contrib/congress/s770/section_8/nii_addon/in_effect.yaml b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/nii_addon/in_effect.yaml new file mode 100644 index 00000000000..0a9f7607ee3 --- /dev/null +++ b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/nii_addon/in_effect.yaml @@ -0,0 +1,11 @@ +description: The NII add-on applies if this is true, independently of the active-income + option. +values: + '0000-01-01': false +metadata: + label: S.770 NII add-on in effect + period: year + reference: + - title: S.770 (119th Congress), sections 8 and 9 + href: https://www.congress.gov/bill/119th-congress/senate-bill/770/text + unit: bool diff --git a/policyengine_us/parameters/gov/contrib/congress/s770/section_8/nii_addon/oasdi_share.yaml b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/nii_addon/oasdi_share.yaml new file mode 100644 index 00000000000..4fc607b34b1 --- /dev/null +++ b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/nii_addon/oasdi_share.yaml @@ -0,0 +1,12 @@ +description: In custom transfer mode, this share of the NII add-on goes to OASDI. The bill + specifies 62 percent of all resulting section 1411 tax; this component share alone does + not reproduce that transfer. +values: + '0000-01-01': 0.62 +metadata: + label: S.770 NII add-on OASDI share + period: year + reference: + - title: S.770 (119th Congress), sections 8 and 9 + href: https://www.congress.gov/bill/119th-congress/senate-bill/770/text + unit: /1 diff --git a/policyengine_us/parameters/gov/contrib/congress/s770/section_8/nii_addon/rate.yaml b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/nii_addon/rate.yaml new file mode 100644 index 00000000000..c521030b3c4 --- /dev/null +++ b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/nii_addon/rate.yaml @@ -0,0 +1,11 @@ +description: The NII option adds this rate to existing NIIT. The bill example is 16.2 percent + minus 3.8 percent. +values: + '0000-01-01': 0.124 +metadata: + label: S.770 NII add-on rate + period: year + reference: + - title: S.770 (119th Congress), sections 8 and 9 + href: https://www.congress.gov/bill/119th-congress/senate-bill/770/text + unit: /1 diff --git a/policyengine_us/parameters/gov/contrib/congress/s770/section_8/nii_addon/start_year.yaml b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/nii_addon/start_year.yaml new file mode 100644 index 00000000000..9a338777bbe --- /dev/null +++ b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/nii_addon/start_year.yaml @@ -0,0 +1,11 @@ +description: The NII add-on starts in this tax year. Section 8(c) specifies taxable years + beginning after enactment, not a fixed year; 2026 is an illustrative enactment assumption. +values: + '0000-01-01': 2026 +metadata: + label: S.770 NII add-on start year + period: year + reference: + - title: S.770 (119th Congress), sections 8 and 9 + href: https://www.congress.gov/bill/119th-congress/senate-bill/770/text + unit: year diff --git a/policyengine_us/parameters/gov/contrib/congress/s770/section_8/transfer/bill_share.yaml b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/transfer/bill_share.yaml new file mode 100644 index 00000000000..0a39aee95c0 --- /dev/null +++ b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/transfer/bill_share.yaml @@ -0,0 +1,11 @@ +description: In bill transfer mode, this share of all resulting section 1411 tax goes to + the merged Social Security fund. +values: + '0000-01-01': 0.62 +metadata: + label: S.770 bill OASDI transfer share + period: year + reference: + - title: S.770 (119th Congress), sections 8 and 9 + href: https://www.congress.gov/bill/119th-congress/senate-bill/770/text + unit: /1 diff --git a/policyengine_us/parameters/gov/contrib/congress/s770/section_8/transfer/mode.yaml b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/transfer/mode.yaml new file mode 100644 index 00000000000..51ee7cd1757 --- /dev/null +++ b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/transfer/mode.yaml @@ -0,0 +1,13 @@ +description: 'OASDI transfer convention: custom uses each incremental part share; combined_rate_share + allocates the OASDI rate over each combined rate; all_addon sends every NII add-on point + to OASDI and uses the active share; bill sends the bill share of the entire resulting + section 1411 tax, including existing NIIT. This selects transfers, not full statutory + category coverage.' +values: + '0000-01-01': custom +metadata: + label: S.770 OASDI transfer mode + period: year + reference: + - title: S.770 (119th Congress), sections 8 and 9 + href: https://www.congress.gov/bill/119th-congress/senate-bill/770/text diff --git a/policyengine_us/parameters/gov/contrib/congress/s770/section_8/transfer/oasdi_rate.yaml b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/transfer/oasdi_rate.yaml new file mode 100644 index 00000000000..25251305c5d --- /dev/null +++ b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/transfer/oasdi_rate.yaml @@ -0,0 +1,12 @@ +description: In combined_rate_share mode, allocate this rate over the enabled combined + NII rate and active tax rate. At bill example rates the OACT convention is 12.4 divided + by 16.2; it differs from the bill share of 62 percent. +values: + '0000-01-01': 0.124 +metadata: + label: S.770 OASDI rate for combined-rate allocation + period: year + reference: + - title: S.770 (119th Congress), sections 8 and 9 + href: https://www.congress.gov/bill/119th-congress/senate-bill/770/text + unit: /1 diff --git a/policyengine_us/parameters/gov/contrib/congress/s770/section_8/transfer/start_year.yaml b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/transfer/start_year.yaml new file mode 100644 index 00000000000..97eb1a726c3 --- /dev/null +++ b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/transfer/start_year.yaml @@ -0,0 +1,12 @@ +description: OASDI transfers start in this modeled annual year. Section 9(a) appropriates + NIIT revenue from the first fiscal year beginning after enactment; this annual tax model + does not prorate fiscal-year receipts. +values: + '0000-01-01': 2026 +metadata: + label: S.770 OASDI transfer start year + period: year + reference: + - title: S.770 (119th Congress), sections 8 and 9 + href: https://www.congress.gov/bill/119th-congress/senate-bill/770/text + unit: year diff --git a/policyengine_us/reforms/congress/s770/README.md b/policyengine_us/reforms/congress/s770/README.md new file mode 100644 index 00000000000..b054965bc7d --- /dev/null +++ b/policyengine_us/reforms/congress/s770/README.md @@ -0,0 +1,143 @@ +# S.770 section 8 split tax options + +These independently enabled options model an NII rate add-on and an additional +active pass-through base. Both default off. Parameters are under +`gov.contrib.congress.s770.section_8`; each part has its own `in_effect`, +`start_year`, rate and custom OASDI share. The example rates are the proposed +bill's 12.4 percentage-point add-on and 16.2 percent active rate. No package +settings are encoded. + +## Sources and dates + +[S.770, 119th Congress, introduced text, sections 8–9](https://www.congress.gov/bill/119th-congress/senate-bill/770/text) +was retrieved October 23, 2026. Section 8(a)(1) replaces 3.8 percent with 16.2 +percent. Section 8(b) broadens the investment-income definition, denies the +section 172 NOL deduction, and excludes income actually taxed under SECA or +FICA. Section 8(c) applies to “taxable years beginning after the date of enactment +of this Act”. There is no statutory fixed start year. The configurable 2026 +defaults illustrate assumed enactment timing and have no effect while disabled. + +Section 9(a) assigns 62 percent of all section 1411 tax to a merged Social +Security fund. It appropriates the revenue beginning with the first fiscal year +after enactment; the merged fund receives assets on the following January 1. +`transfer.start_year` is a separate annual modeling choice. This liability model +does not estimate fiscal-year collection lags or prorate receipts. + +## Signed base and tax accounting + +Use the existing `niit_magi`, including its modeled estate and section 911 +adjustments, and existing filing-status thresholds. Let `X=max(MAGI−threshold,0)`, +`N` be signed adjusted old-definition NII, and `A` the signed additional active +base (zero if that option is off): + +``` +L = min(max(N + A, 0), X) +n = min(max(N, 0), L) +a = L - n +tax = (existing_rate + enabled_addon_rate) * n + enabled_active_rate * a +``` + +Losses net before clipping. Both allocated shares are nonnegative; NII consumes +the limit first even when its add-on rate is zero. When active inclusion is on, +the existing 3.8 percent NIIT also uses `n`. The reform replaces +`net_investment_income_tax` with the entire resulting liability. The two named +component taxes are already included there and in federal income tax; adding +them again would double count. + +With identical sources, exclusions, NOL treatment, MAGI, thresholds and timing, +and `existing_rate + addon_rate = active_rate = .162`, this equals `.162 * L` +for all signed inputs. This is a conditional liability identity, not a claim +that the available fields implement every statutory category. For `N=−10,000`, +`A=20,000`, `X=50,000`, the tax is $1,620. For `N=20,000`, `A=−10,000`, the +tax is also $1,620. Active losses can therefore reduce liability relative to +the add-on alone ($3,240 in the latter example). Activation is monotone with +nonnegative active income and nonnegative rates, not for arbitrary signed losses. + +## Pass-through classification and SECA/FICA interaction + +The person-level total is `partnership_s_corp_income`, whose formula sums +`s_corp_income + partnership_income`. Subtract the passive subset and gross +already-SE active subset, then net signed results across head/spouse members. +Dependents' income belongs to their own return. This assumes the passive and +already-SE subsets are disjoint unless their validated intersection is supplied +in `s770_passive_se_income`. An intersection is removed from NII too. Signed +subsets are not independently floored. Cross-entity netting within the available +person totals is an approximation. Nonpassive status is only a proxy for material +participation, not a determination of each limited partner's legal status. + +The already-SE exclusion uses `partnership_self_employment_net_earnings`, not a +capped OASDI base: existing SE earnings can still pay uncapped HI above that cap. +Separately paid wages are outside distributive entity income. If supplied totals +embed FICA-taxed amounts, use the validated `s770_active_fica_income` exclusion; +use `s770_nii_fica_seca_income` for other covered amounts already in NII, excluding +the separately supplied passive/SE intersection. These categories and exclusions +must be disjoint and supported by source data; the model cannot infer overlap. + +For a future owners-draw reform, the `s770_draw_*` inputs are classification hooks +and do not impose SECA themselves. The supplier must connect actual taxed amounts: + +* OASDI-only: exclude `s770_draw_oasdi_base / q`, where the draw base is after + wages, existing SE earnings and applicable cap room. Above-room profits remain + in `A` because no SECA applies to them. +* Uncapped draw HI: exclude the full actual `s770_draw_gross`. + +Here `q=1−employer_share*(SE_OASDI_rate+SE_HI_rate)`, currently .9235, using +existing parameters. The undrawn phase-in share and floor-rejected planned draw +remain in `A`. No independent owners-draw policy is enabled by these hooks. + +## Literal-bill category audit and NOL support + +| Statutory category | Available treatment and limitation | +| --- | --- | +| Interest, dividends, annuities, royalties and rents, including ordinary-course business sources after §8(b)(1)(A) | Existing NII sources are reused. Separate ordinary-course/SECA/FICA classification, annuity and royalty completeness are not established. | +| Trade/business income under amended §1411(c)(1)(A)(ii) | Active pass-through residual is added. All other business categories and material-participation determinations are not inferred. | +| Net property-disposition gains, including business property after §8(b)(1)(C) | Existing capital-gain sources are reused. Full business-property gain and associated exclusion classification is unsupported. | +| §172 deduction denial under §8(b)(2) | No federal §172 NOL deduction input/formula exists in the baseline NII source list. Indiana NOL variables are state-specific. | +| Items actually taxed by §1401 or §3101 | Already-SE fields and explicit validated overlap/FICA/draw hooks provide exclusions, not a complete statutory source classifier. | + +`s770_niit_nol_deduction` is an explicit TaxUnit input for a validated deduction +**already subtracted from supplied NII**. When active inclusion and `deny_nol` +are on, it is added back to `N` before unified allocation, changing broadened NII +and the existing NIIT component too. Do not supply an AGI-only deduction or treat +ordinary business losses as an NOL deduction. Baseline formulas deduct no federal +NOL from NII, so the default input is zero. Complete literal-bill coverage needs +a validated federal deduction/data route and the missing category classification; +neither a zero input nor matching rates/transfers establishes that coverage. + +## OASDI transfers + +Transfers are separate from liability. `transfer.mode` selects: + +| Mode | Existing NIIT | NII add-on | Active tax | +| --- | --- | --- | --- | +| `custom` (default) | 0 | `nii_addon.oasdi_share` | `active_business.oasdi_share` | +| `combined_rate_share` | OASDI rate / combined NII rate | Same combined share | OASDI rate / active rate | +| `all_addon` | 0 | 100% | `active_business.oasdi_share` | +| `bill` | `transfer.bill_share` | Same bill share | Same bill share | + +The OACT-style 12.4/16.2 convention is approximately 76.5432 percent, distinct +from the bill's 62 percent. A combined-rate share requires an OASDI rate no +larger than each positive tax rate to which it applies; a zero tax has zero +transfer. When the NII add-on is off or its rate is zero, this custom mode +transfers no existing NIIT, while the active transfer remains independent. +`bill` selects the bill's **transfer rule**, not unsupported category +coverage. Custom part shares default to .62; custom mode does not transfer the +existing 3.8 percent component. Bill mode does, through the separately reported +`s770_existing_niit_revenue_oasdi`. All transfers are zero when both options are +off or before the transfer start year. + +`s770_nii_addon_revenue_oasdi`, `s770_active_business_income_revenue_oasdi`, and +`s770_existing_niit_revenue_oasdi` sum to `s770_revenue_oasdi`. Each is its selected +mode's share times the corresponding tax. Aggregate each TaxUnit once alongside +`tob_revenue_oasdi`. Liability equivalence does not establish allocation equivalence. + +## Scoring-data limitation + +The certified pre-repair artifact forces `s_corp_income` to zero, puts combined +income carrying passive mass in `partnership_income`, and omits +`passive_partnership_s_corp_income` (which therefore defaults to zero). The active +formula runs on those fields, but (b) overstates active income while (a) +understates NII because passive pass-through income is missing. Microcosm #1251 +repairs total/passive fields; #1261 splits the entities. Scoring remains +provisional until the selected artifact actually contains both repairs and its +stored fields are checked. No population effects are asserted here. diff --git a/policyengine_us/reforms/congress/s770/__init__.py b/policyengine_us/reforms/congress/s770/__init__.py new file mode 100644 index 00000000000..ae73789202e --- /dev/null +++ b/policyengine_us/reforms/congress/s770/__init__.py @@ -0,0 +1 @@ +from .section_8 import create_s770_section_8_reform diff --git a/policyengine_us/reforms/congress/s770/section_8.py b/policyengine_us/reforms/congress/s770/section_8.py new file mode 100644 index 00000000000..1cd588b19d1 --- /dev/null +++ b/policyengine_us/reforms/congress/s770/section_8.py @@ -0,0 +1,122 @@ +"""Independent S.770 section 8 options; see README.md for scope and accounting.""" + +import numpy as np + +from policyengine_us.model_api import * + + +def enabled(part, period): + return part.in_effect and period.start.year >= part.start_year + + +def active_residual(total, passive, already_se, draw_exclusion=0): + """Signed profits, assuming the passive and already-SE subsets are disjoint.""" + return total - passive - already_se - draw_exclusion + + +def allocate_bases(nii, active, excess): + """Net signed sources before clipping, then allocate the limit to NII first.""" + limited = np.minimum(np.maximum(nii + active, 0), np.maximum(excess, 0)) + nii_base = np.minimum(np.maximum(nii, 0), limited) + return nii_base, limited - nii_base + + +def transfer_shares( + mode, + existing_rate, + addon_rate, + active_rate, + nii_share, + active_share, + bill_share, + oasdi_rate, +): + """Shares of existing NIIT, the add-on, and active tax, respectively.""" + if mode == "custom": + return 0, nii_share, active_share + if mode == "bill": + return bill_share, bill_share, bill_share + if mode == "all_addon": + return 0, 1, active_share + if mode == "combined_rate_share": + combined = existing_rate + addon_rate + # With no NII add-on, this custom mode transfers no old-law NII + # receipts. The independently enabled active option still transfers. + nii = oasdi_rate / combined if addon_rate and combined else 0 + active = oasdi_rate / active_rate if active_rate else 0 + if not (0 <= nii <= 1 and 0 <= active <= 1): + raise ValueError("OASDI rate must not exceed an enabled combined tax rate") + return nii, nii, active + raise ValueError(f"Unknown S.770 transfer mode: {mode}") + + +def revenue_shares(parameters, period): + p = parameters(period).gov.contrib.congress.s770.section_8 + if period.start.year < p.transfer.start_year: + return 0, 0, 0 + nii_on = enabled(p.nii_addon, period) + active_on = enabled(p.active_business, period) + if not (nii_on or active_on): + return 0, 0, 0 + existing_rate = parameters(period).gov.irs.investment.net_investment_income_tax.rate + return transfer_shares( + p.transfer.mode, + existing_rate, + p.nii_addon.rate if nii_on else 0, + p.active_business.rate if active_on else 0, + p.nii_addon.oasdi_share, + p.active_business.oasdi_share, + p.transfer.bill_share, + p.transfer.oasdi_rate, + ) + + +def create_s770_section_8(): + class net_investment_income_tax(Variable): + value_type = float + entity = TaxUnit + definition_period = YEAR + label = "Net investment income tax including enabled S.770 section 8 options" + unit = USD + reference = "https://www.congress.gov/bill/119th-congress/senate-bill/770/text" + documentation = ( + "Entire resulting section 1411 liability, including the existing rate. " + "When the active option is enabled, signed active losses also change " + "the existing NIIT base. The named S.770 component taxes are already " + "included here and must not be added to income tax a second time." + ) + + def formula(tax_unit, period, parameters): + return add( + tax_unit, + period, + [ + "s770_existing_niit_tax", + "s770_nii_addon_tax", + "s770_active_business_income_tax", + ], + ) + + class reform(Reform): + def apply(self): + self.update_variable(net_investment_income_tax) + + return reform + + +def create_s770_section_8_reform(parameters, period, bypass=False): + if bypass: + return create_s770_section_8() + p = parameters.gov.contrib.congress.s770.section_8 + # Install once if enabled anywhere in the parameter history. Formula gates + # retain both independent start years, including requests in another order. + if any( + value.value + for part in (p.nii_addon, p.active_business) + for value in part.in_effect.values_list + ): + return create_s770_section_8() + return None + + +section_8 = create_s770_section_8_reform(None, None, bypass=True) diff --git a/policyengine_us/reforms/reforms.py b/policyengine_us/reforms/reforms.py index 61fcd3050de..422e304b734 100644 --- a/policyengine_us/reforms/reforms.py +++ b/policyengine_us/reforms/reforms.py @@ -1,3 +1,4 @@ +from .congress.s770 import create_s770_section_8_reform from .congress.delauro import create_american_family_act_with_baby_bonus_reform from .states.al.hb527 import create_al_hb527_overtime_deduction_reform from .dc_kccatc import create_dc_kccatc_reform @@ -601,7 +602,10 @@ def create_structural_reforms_from_parameters(parameters, period): parameters, period ) + s770_section_8 = create_s770_section_8_reform(parameters, period) + reforms = [ + s770_section_8, afa_reform, winship_reform, dc_kccatc_reform, diff --git a/policyengine_us/tests/policy/contrib/congress/s770/section_8.yaml b/policyengine_us/tests/policy/contrib/congress/s770/section_8.yaml new file mode 100644 index 00000000000..02a25ff254d --- /dev/null +++ b/policyengine_us/tests/policy/contrib/congress/s770/section_8.yaml @@ -0,0 +1,698 @@ +- name: Active S corporation shareholder with 500000 of income + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.active_business.in_effect: true + s_corp_income: 500_000 + niit_magi: 500_000 + filing_status: SINGLE + output: + s770_active_business_income: 500_000 + s770_nii_taxable_base: 0 + s770_active_taxable_base: 300_000 + s770_nii_addon_tax: 0 + # 16.2% x min(500,000, 500,000 - 200,000). + s770_active_business_income_tax: 48_600 + net_investment_income_tax: 48_600 + s770_active_business_income_revenue_oasdi: 30_132 + s770_revenue_oasdi: 30_132 + +- name: Limited partner excludes the passive subset from the active residual + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.active_business.in_effect: true + partnership_income: 300_000 + passive_partnership_s_corp_income: 100_000 + niit_magi: 400_000 + filing_status: SINGLE + output: + partnership_s_corp_income: 300_000 + net_investment_income: 100_000 + s770_nii_base: 100_000 + s770_active_business_income: 200_000 + # NII consumes 100,000 of the shared 200,000 MAGI excess first. + s770_nii_taxable_base: 100_000 + s770_active_taxable_base: 100_000 + s770_nii_addon_tax: 0 + s770_active_business_income_tax: 16_200 + net_investment_income_tax: 20_000 + +- name: General partner earnings already subject to SECA have no new active tax + period: 2026 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.active_business.in_effect: true + partnership_income: 300_000 + partnership_self_employment_net_earnings: 300_000 + niit_magi: 500_000 + filing_status: SINGLE + output: + s770_active_business_income: 0 + s770_active_business_income_tax: 0 + net_investment_income_tax: 0 + +- name: Wages subject to FICA do not enter either new tax base + period: 2026 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.active_business.in_effect: true + employment_income: 500_000 + filing_status: SINGLE + output: + s770_nii_base: 0 + s770_active_business_income: 0 + s770_nii_addon_tax: 0 + s770_active_business_income_tax: 0 + net_investment_income_tax: 0 + +- name: Below threshold neither part imposes tax + period: 2026 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.active_business.in_effect: true + taxable_interest_income: 50_000 + s_corp_income: 100_000 + niit_magi: 199_999 + filing_status: SINGLE + output: + s770_nii_taxable_base: 0 + s770_active_taxable_base: 0 + s770_nii_addon_tax: 0 + s770_active_business_income_tax: 0 + net_investment_income_tax: 0 + s770_revenue_oasdi: 0 + +- name: Both switches off preserve current NIIT on the same household + period: 2026 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + taxable_interest_income: 100_000 + s_corp_income: 200_000 + niit_magi: 400_000 + filing_status: SINGLE + output: + s770_nii_addon_tax: 0 + s770_active_business_income_tax: 0 + net_investment_income_tax: 3_800 + s770_revenue_oasdi: 0 + +- name: NII addon alone on the same household + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + taxable_interest_income: 100_000 + s_corp_income: 200_000 + niit_magi: 400_000 + filing_status: SINGLE + output: + s770_nii_taxable_base: 100_000 + s770_active_taxable_base: 0 + s770_nii_addon_tax: 12_400 + s770_active_business_income_tax: 0 + net_investment_income_tax: 16_200 + s770_nii_addon_revenue_oasdi: 7_688 + s770_existing_niit_revenue_oasdi: 0 + s770_revenue_oasdi: 7_688 + +- name: Active option alone on the same household + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.active_business.in_effect: true + taxable_interest_income: 100_000 + s_corp_income: 200_000 + niit_magi: 400_000 + filing_status: SINGLE + output: + s770_nii_taxable_base: 100_000 + s770_active_taxable_base: 100_000 + s770_nii_addon_tax: 0 + s770_active_business_income_tax: 16_200 + net_investment_income_tax: 20_000 + s770_active_business_income_revenue_oasdi: 10_044 + s770_revenue_oasdi: 10_044 + +- name: Both options on the same household share one MAGI excess + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.active_business.in_effect: true + taxable_interest_income: 100_000 + s_corp_income: 200_000 + niit_magi: 400_000 + filing_status: SINGLE + output: + s770_nii_taxable_base: 100_000 + s770_active_taxable_base: 100_000 + s770_nii_addon_tax: 12_400 + s770_active_business_income_tax: 16_200 + # .162 x min(100,000 + 200,000, 400,000 - 200,000). + net_investment_income_tax: 32_400 + s770_nii_addon_revenue_oasdi: 7_688 + s770_active_business_income_revenue_oasdi: 10_044 + s770_existing_niit_revenue_oasdi: 0 + s770_revenue_oasdi: 17_732 + +- name: Zero addon rate still reserves NII threshold room + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.nii_addon.rate: 0 + gov.contrib.congress.s770.section_8.active_business.in_effect: true + taxable_interest_income: 20_000 + s_corp_income: 20_000 + niit_magi: 230_000 + filing_status: SINGLE + output: + s770_nii_taxable_base: 20_000 + s770_active_taxable_base: 10_000 + s770_nii_addon_tax: 0 + s770_active_business_income_tax: 1_620 + # .038 x 20,000 + .162 x 10,000. + net_investment_income_tax: 2_380 + +- name: Zero active rate does not change the NII allocation + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.active_business.in_effect: true + gov.contrib.congress.s770.section_8.active_business.rate: 0 + taxable_interest_income: 20_000 + s_corp_income: 20_000 + niit_magi: 230_000 + filing_status: SINGLE + output: + s770_nii_taxable_base: 20_000 + s770_active_taxable_base: 10_000 + s770_nii_addon_tax: 2_480 + s770_active_business_income_tax: 0 + net_investment_income_tax: 3_240 + +- name: Signed NII loss offsets active income before either bucket is clipped + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.active_business.in_effect: true + estate_income: -10_000 + s_corp_income: 20_000 + niit_magi: 250_000 + filing_status: SINGLE + output: + s770_nii_base: -10_000 + s770_active_business_income: 20_000 + s770_nii_taxable_base: 0 + s770_active_taxable_base: 10_000 + s770_nii_addon_tax: 0 + # Review counterexample: .162 x (-10,000 + 20,000), not .162 x 20,000. + s770_active_business_income_tax: 1_620 + net_investment_income_tax: 1_620 + +- name: Signed active loss also reduces the existing NIIT component + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.active_business.in_effect: true + taxable_interest_income: 20_000 + s_corp_income: -10_000 + niit_magi: 250_000 + filing_status: SINGLE + output: + s770_nii_base: 20_000 + s770_active_business_income: -10_000 + s770_nii_taxable_base: 10_000 + s770_active_taxable_base: 0 + s770_nii_addon_tax: 1_240 + s770_active_business_income_tax: 0 + net_investment_income_tax: 1_620 + +- name: A net unified loss produces no resulting section 1411 liability + period: 2026 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.active_business.in_effect: true + estate_income: -30_000 + s_corp_income: 20_000 + niit_magi: 250_000 + filing_status: SINGLE + output: + s770_nii_base: -30_000 + s770_active_business_income: 20_000 + s770_nii_taxable_base: 0 + s770_active_taxable_base: 0 + s770_nii_addon_tax: 0 + s770_active_business_income_tax: 0 + net_investment_income_tax: 0 + +- name: An inactive active option does not import its signed loss + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + taxable_interest_income: 20_000 + s_corp_income: -10_000 + niit_magi: 250_000 + filing_status: SINGLE + output: + s770_nii_taxable_base: 20_000 + s770_active_taxable_base: 0 + s770_nii_addon_tax: 2_480 + s770_active_business_income_tax: 0 + net_investment_income_tax: 3_240 + +- name: NOL denial restores the deduction to broadened NII before allocation + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.active_business.in_effect: true + # This supplied NII already deducts 30,000 under section 172. + net_investment_income: -10_000 + s770_niit_nol_deduction: 30_000 + s_corp_income: 20_000 + niit_magi: 250_000 + filing_status: SINGLE + output: + s770_nii_base: 20_000 + s770_active_business_income: 20_000 + s770_nii_taxable_base: 20_000 + s770_active_taxable_base: 20_000 + s770_nii_addon_tax: 2_480 + s770_active_business_income_tax: 3_240 + net_investment_income_tax: 6_480 + +- name: Custom NOL permission preserves the supplied deduction + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.active_business.in_effect: true + gov.contrib.congress.s770.section_8.active_business.deny_nol: false + net_investment_income: -10_000 + s770_niit_nol_deduction: 30_000 + s_corp_income: 20_000 + niit_magi: 250_000 + filing_status: SINGLE + output: + s770_nii_base: -10_000 + s770_nii_taxable_base: 0 + s770_active_taxable_base: 10_000 + s770_nii_addon_tax: 0 + s770_active_business_income_tax: 1_620 + net_investment_income_tax: 1_620 + +- name: NII addon alone does not activate the active option NOL denial + period: 2026 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + net_investment_income: -10_000 + s770_niit_nol_deduction: 30_000 + s_corp_income: 20_000 + niit_magi: 250_000 + filing_status: SINGLE + output: + s770_nii_base: -10_000 + s770_nii_addon_tax: 0 + s770_active_business_income_tax: 0 + net_investment_income_tax: 0 + +- name: Both options respect the bill start year + period: 2025 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.active_business.in_effect: true + taxable_interest_income: 100_000 + s_corp_income: 200_000 + niit_magi: 400_000 + filing_status: SINGLE + output: + s770_nii_addon_tax: 0 + s770_active_business_income_tax: 0 + net_investment_income_tax: 3_800 + s770_revenue_oasdi: 0 + +- name: The active option can start later than the NII addon + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.active_business.in_effect: true + gov.contrib.congress.s770.section_8.active_business.start_year: 2027 + taxable_interest_income: 100_000 + s_corp_income: 200_000 + niit_magi: 400_000 + filing_status: SINGLE + output: + s770_nii_addon_tax: 12_400 + s770_active_business_income_tax: 0 + net_investment_income_tax: 16_200 + +- name: The NII addon can start later than the active option + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.nii_addon.start_year: 2027 + gov.contrib.congress.s770.section_8.active_business.in_effect: true + taxable_interest_income: 100_000 + s_corp_income: 200_000 + niit_magi: 400_000 + filing_status: SINGLE + output: + s770_nii_addon_tax: 0 + s770_active_business_income_tax: 16_200 + net_investment_income_tax: 20_000 + +- name: Bill transfer mode sends 62 percent of all resulting NIIT to OASDI + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.active_business.in_effect: true + gov.contrib.congress.s770.section_8.transfer.mode: bill + taxable_interest_income: 100_000 + s_corp_income: 200_000 + niit_magi: 400_000 + filing_status: SINGLE + output: + net_investment_income_tax: 32_400 + s770_existing_niit_revenue_oasdi: 2_356 + s770_nii_addon_revenue_oasdi: 7_688 + s770_active_business_income_revenue_oasdi: 10_044 + s770_revenue_oasdi: 20_088 + +- name: Combined rate allocation sends 12.4 of 16.2 percentage points to OASDI + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.active_business.in_effect: true + gov.contrib.congress.s770.section_8.transfer.mode: combined_rate_share + taxable_interest_income: 100_000 + s_corp_income: 200_000 + niit_magi: 400_000 + filing_status: SINGLE + output: + net_investment_income_tax: 32_400 + s770_existing_niit_revenue_oasdi: 3_800 * .124 / .162 + s770_nii_addon_revenue_oasdi: 12_400 * .124 / .162 + s770_active_business_income_revenue_oasdi: 12_400 + s770_revenue_oasdi: 24_800 + +- name: All addon mode transfers all new NII points and the chosen active share + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.active_business.in_effect: true + gov.contrib.congress.s770.section_8.transfer.mode: all_addon + taxable_interest_income: 100_000 + s_corp_income: 200_000 + niit_magi: 400_000 + filing_status: SINGLE + output: + s770_existing_niit_revenue_oasdi: 0 + s770_nii_addon_revenue_oasdi: 12_400 + s770_active_business_income_revenue_oasdi: 10_044 + s770_revenue_oasdi: 22_444 + +- name: Custom transfer shares remain independently configurable + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.nii_addon.oasdi_share: .5 + gov.contrib.congress.s770.section_8.active_business.in_effect: true + gov.contrib.congress.s770.section_8.active_business.oasdi_share: .25 + taxable_interest_income: 100_000 + s_corp_income: 200_000 + niit_magi: 400_000 + filing_status: SINGLE + output: + s770_existing_niit_revenue_oasdi: 0 + s770_nii_addon_revenue_oasdi: 6_200 + s770_active_business_income_revenue_oasdi: 4_050 + s770_revenue_oasdi: 10_250 + +- name: Bill transfer has its own start year + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.active_business.in_effect: true + gov.contrib.congress.s770.section_8.transfer.mode: bill + gov.contrib.congress.s770.section_8.transfer.start_year: 2027 + taxable_interest_income: 100_000 + s_corp_income: 200_000 + niit_magi: 400_000 + filing_status: SINGLE + output: + net_investment_income_tax: 32_400 + s770_existing_niit_revenue_oasdi: 0 + s770_nii_addon_revenue_oasdi: 0 + s770_active_business_income_revenue_oasdi: 0 + s770_revenue_oasdi: 0 + +- name: Head and spouse aggregate but dependent income stays outside both bases + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.active_business.in_effect: true + people: + head: + age: 45 + is_tax_unit_head: true + taxable_interest_income: 10_000 + s_corp_income: 100_000 + spouse: + age: 45 + is_tax_unit_spouse: true + taxable_interest_income: 20_000 + partnership_income: 200_000 + dependent: + age: 15 + is_tax_unit_dependent: true + taxable_interest_income: 50_000 + s_corp_income: 500_000 + tax_units: + family: + members: [head, spouse, dependent] + filing_status: JOINT + niit_magi: 350_000 + households: + household: + members: [head, spouse, dependent] + state_code: TX + output: + s770_nii_base: 30_000 + s770_active_business_income: 300_000 + s770_nii_taxable_base: 30_000 + s770_active_taxable_base: 70_000 + s770_nii_addon_tax: 3_720 + s770_active_business_income_tax: 11_340 + net_investment_income_tax: 16_200 + +- name: OASDI only draw above cap room excludes only profits actually taxed + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.active_business.in_effect: true + s_corp_income: 200_000 + s770_draw_gross: 150_000 + # q=.9235; 9,235 of covered net earnings represents 10,000 gross profit. + s770_draw_oasdi_base: 9_235 + s770_draw_hi_in_effect: false + niit_magi: 400_000 + filing_status: SINGLE + output: + s770_active_business_income: 190_000 + s770_active_taxable_base: 190_000 + s770_active_business_income_tax: 30_780 + net_investment_income_tax: 30_780 + +- name: HI draw above cap room excludes the full gross draw from the active base + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.active_business.in_effect: true + s_corp_income: 200_000 + s770_draw_gross: 150_000 + s770_draw_oasdi_base: 9_235 + s770_draw_hi_in_effect: true + niit_magi: 400_000 + filing_status: SINGLE + output: + s770_active_business_income: 50_000 + s770_active_taxable_base: 50_000 + s770_active_business_income_tax: 8_100 + net_investment_income_tax: 8_100 + +- name: OASDI only draw with no remaining cap room leaves all profits active + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.active_business.in_effect: true + s_corp_income: 200_000 + s770_draw_gross: 150_000 + s770_draw_oasdi_base: 0 + s770_draw_hi_in_effect: false + niit_magi: 400_000 + filing_status: SINGLE + output: + s770_active_business_income: 200_000 + s770_active_taxable_base: 200_000 + s770_active_business_income_tax: 32_400 + net_investment_income_tax: 32_400 + +- name: Explicit passive SE overlap is removed once from each affected category + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.active_business.in_effect: true + partnership_income: 300_000 + passive_partnership_s_corp_income: 100_000 + partnership_self_employment_net_earnings: 150_000 + s770_passive_se_income: 40_000 + niit_magi: 400_000 + filing_status: SINGLE + output: + # N = 100,000 - 40,000; A = 300,000 - 100,000 - (150,000 - 40,000). + s770_nii_base: 60_000 + s770_active_business_income: 90_000 + s770_nii_taxable_base: 60_000 + s770_active_taxable_base: 90_000 + s770_nii_addon_tax: 7_440 + s770_active_business_income_tax: 14_580 + net_investment_income_tax: 24_300 + +- name: Existing filing status thresholds govern both options + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.active_business.in_effect: true + people: + single: + age: 40 + taxable_interest_income: 20_000 + s_corp_income: 20_000 + joint: + age: 40 + taxable_interest_income: 20_000 + s_corp_income: 20_000 + separate: + age: 40 + taxable_interest_income: 20_000 + s_corp_income: 20_000 + head: + age: 40 + taxable_interest_income: 20_000 + s_corp_income: 20_000 + survivor: + age: 40 + taxable_interest_income: 20_000 + s_corp_income: 20_000 + tax_units: + single: + members: [single] + filing_status: SINGLE + niit_magi: 230_000 + joint: + members: [joint] + filing_status: JOINT + niit_magi: 280_000 + separate: + members: [separate] + filing_status: SEPARATE + niit_magi: 155_000 + head: + members: [head] + filing_status: HEAD_OF_HOUSEHOLD + niit_magi: 230_000 + survivor: + members: [survivor] + filing_status: SURVIVING_SPOUSE + niit_magi: 280_000 + households: + household: + members: [single, joint, separate, head, survivor] + state_code: TX + output: + # Each filing status has 30,000 of MAGI excess: n=20,000, a=10,000. + s770_nii_taxable_base: [20_000, 20_000, 20_000, 20_000, 20_000] + s770_active_taxable_base: [10_000, 10_000, 10_000, 10_000, 10_000] + s770_nii_addon_tax: [2_480, 2_480, 2_480, 2_480, 2_480] + s770_active_business_income_tax: [1_620, 1_620, 1_620, 1_620, 1_620] + net_investment_income_tax: [4_860, 4_860, 4_860, 4_860, 4_860] + +- name: Exact threshold is untaxed and one dollar above it has one dollar of base + period: 2026 + absolute_error_margin: 0.001 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.active_business.in_effect: true + people: + exact: + age: 40 + s_corp_income: 100_000 + above: + age: 40 + s_corp_income: 100_000 + tax_units: + exact: + members: [exact] + filing_status: SINGLE + niit_magi: 200_000 + above: + members: [above] + filing_status: SINGLE + niit_magi: 200_001 + households: + household: + members: [exact, above] + state_code: TX + output: + s770_active_taxable_base: [0, 1] + s770_nii_addon_tax: [0, 0] + s770_active_business_income_tax: [0, .162] + net_investment_income_tax: [0, .162] diff --git a/policyengine_us/tests/reforms/test_s770_section_8.py b/policyengine_us/tests/reforms/test_s770_section_8.py new file mode 100644 index 00000000000..a4e7bc44c88 --- /dev/null +++ b/policyengine_us/tests/reforms/test_s770_section_8.py @@ -0,0 +1,389 @@ +"""Signed allocation properties and multi-year S.770 reform integration. + +The inexpensive Hypothesis cases exercise vector helpers without rebuilding +the model. One shared, read-only simulation then checks registration, changing +switches/start years, entity aggregation and revenue accounting across years; +YAML cannot express those dated parameter changes. No population data are used. + +Unified-liability equivalence assumes identical signed sources, exclusions, +NOL treatment, MAGI, thresholds and timing, with existing rate + add-on rate = +active rate = 16.2%. It does not imply equal OASDI transfers or full statutory +category coverage. Adding a signed active loss can lower total tax, so the +monotonicity property requires a nonnegative active residual. +""" + +from types import SimpleNamespace + +import numpy as np +import pytest + +from policyengine_core.reforms import Reform +from policyengine_us import Simulation +from policyengine_us.reforms.congress.s770.section_8 import ( + active_residual, + allocate_bases, + enabled, + transfer_shares, +) + +hypothesis = pytest.importorskip("hypothesis") +st = pytest.importorskip("hypothesis.strategies") + +EXISTING_RATE = 0.038 +ADDON_RATE = 0.124 +ACTIVE_RATE = 0.162 +SHARE = 0.62 +YEARS = tuple(range(2025, 2035)) +MODES = ("custom", "bill", "combined_rate_share", "all_addon") +SETTINGS = hypothesis.settings(max_examples=100, deadline=None) +SIGNED = st.integers(-1_000_000, 1_000_000) +NONNEGATIVE = st.integers(0, 1_000_000) +BASES = st.lists(st.tuples(SIGNED, SIGNED, NONNEGATIVE), min_size=1, max_size=30) + + +@SETTINGS +@hypothesis.given(BASES) +def test_signed_unified_liability_equivalence(rows): + nii, active, excess = np.asarray(rows, dtype=float).T + n, a = allocate_bases(nii, active, excess) + unified = np.minimum(np.maximum(nii + active, 0), excess) + total = EXISTING_RATE * n + ADDON_RATE * n + ACTIVE_RATE * a + np.testing.assert_allclose(total, ACTIVE_RATE * unified, atol=1e-8) + np.testing.assert_allclose(n + a, unified) + assert np.all(total >= 0) + assert np.all(n >= 0) + assert np.all(a >= 0) + assert np.all(n <= np.maximum(nii, 0)) + # Signed sources are netted first; the resulting allocated shares are + # both nonnegative under E2's allocation, even when a source is negative. + np.testing.assert_allclose(a[n < np.maximum(nii, 0)], 0) + + +def test_review_counterexamples_and_zero_addon_threshold_allocation(): + n, a = allocate_bases( + np.array([-10_000, 20_000, 20_000]), + np.array([20_000, -10_000, 20_000]), + np.array([50_000, 50_000, 30_000]), + ) + np.testing.assert_array_equal(n, [0, 10_000, 20_000]) + np.testing.assert_array_equal(a, [10_000, 0, 10_000]) + np.testing.assert_allclose(ACTIVE_RATE * (n + a), [1_620, 1_620, 4_860]) + assert EXISTING_RATE * n[2] + ACTIVE_RATE * a[2] == pytest.approx(2_380) + # An active loss reduces the existing 3.8% component as well as the + # add-on; unconditional monotonicity would contradict signed netting. + assert ACTIVE_RATE * (n[1] + a[1]) < ACTIVE_RATE * 20_000 + + +@SETTINGS +@hypothesis.given(nii=SIGNED, active=NONNEGATIVE, excess=NONNEGATIVE) +def test_turning_on_parts_is_monotone_for_nonnegative_active_income( + nii, active, excess +): + n, a = allocate_bases(nii, active, excess) + n_without_active, _ = allocate_bases(nii, 0, excess) + baseline = EXISTING_RATE * n_without_active + addon_only = (EXISTING_RATE + ADDON_RATE) * n_without_active + active_only = EXISTING_RATE * n + ACTIVE_RATE * a + both = (EXISTING_RATE + ADDON_RATE) * n + ACTIVE_RATE * a + assert addon_only >= baseline + assert active_only >= baseline + assert both >= addon_only + assert both >= active_only + + +@SETTINGS +@hypothesis.given( + active=NONNEGATIVE, + passive=NONNEGATIVE, + already_se=NONNEGATIVE, + draw_exclusion=NONNEGATIVE, +) +def test_no_profit_is_in_both_seca_and_active_residual( + active, passive, already_se, draw_exclusion +): + # Disjoint source subsets are drawn independently. The two SECA-covered + # subsets leave exactly the undrawn, nonpassive active dollars behind. + total = active + passive + already_se + draw_exclusion + residual = active_residual(total, passive, already_se, draw_exclusion) + assert residual == active + assert residual + already_se + draw_exclusion == total - passive + + +@SETTINGS +@hypothesis.given(total=SIGNED, passive=SIGNED, already_se=SIGNED) +def test_active_residual_preserves_signed_losses(total, passive, already_se): + assert active_residual(total, passive, already_se) + passive + already_se == total + + +@SETTINGS +@hypothesis.given( + mode=st.sampled_from(MODES), + nii_share=st.integers(0, 10_000), + active_share=st.integers(0, 10_000), + bill_share=st.integers(0, 10_000), + nii_tax=NONNEGATIVE, + addon_tax=NONNEGATIVE, + active_tax=NONNEGATIVE, +) +def test_transfer_modes_account_for_each_component( + mode, nii_share, active_share, bill_share, nii_tax, addon_tax, active_tax +): + nii_share, active_share, bill_share = ( + value / 10_000 for value in (nii_share, active_share, bill_share) + ) + shares = transfer_shares( + mode, + EXISTING_RATE, + ADDON_RATE, + ACTIVE_RATE, + nii_share, + active_share, + bill_share, + ADDON_RATE, + ) + expected = { + "custom": (0, nii_share, active_share), + "bill": (bill_share,) * 3, + "combined_rate_share": (ADDON_RATE / ACTIVE_RATE,) * 3, + "all_addon": (0, 1, active_share), + }[mode] + np.testing.assert_allclose(shares, expected) + taxes = np.array([nii_tax, addon_tax, active_tax]) + np.testing.assert_allclose(np.asarray(shares) * taxes, np.asarray(expected) * taxes) + if mode == "bill": + assert np.dot(shares, taxes) == pytest.approx(bill_share * taxes.sum()) + + +def test_combined_rate_transfer_handles_zero_rates(): + assert transfer_shares("combined_rate_share", 0, 0, 0, SHARE, SHARE, SHARE, 0) == ( + 0, + 0, + 0, + ) + + +@SETTINGS +@hypothesis.given( + in_effect=st.booleans(), + start_year=st.integers(2026, 2040), + year=st.integers(2020, 2045), +) +def test_activation_requires_switch_and_start_year(in_effect, start_year, year): + part = SimpleNamespace(in_effect=in_effect, start_year=start_year) + period = SimpleNamespace(start=SimpleNamespace(year=year)) + assert bool(enabled(part, period)) == (in_effect and year >= start_year) + + +class DatedSection8Options(Reform): + """Exercise the public parameter route, including structural registration.""" + + def apply(self): + def modify(parameters): + p = parameters.gov.contrib.congress.s770.section_8 + for year in YEARS: + values = { + "nii_addon.in_effect": year not in (2027, 2033), + "active_business.in_effect": year not in (2026, 2033), + "nii_addon.start_year": 2026, + "active_business.start_year": 2026, + "nii_addon.rate": 0 if year == 2032 else ADDON_RATE, + "active_business.rate": ACTIVE_RATE, + "nii_addon.oasdi_share": SHARE, + "active_business.oasdi_share": SHARE, + "active_business.deny_nol": year != 2032, + "transfer.mode": { + 2029: "bill", + 2030: "combined_rate_share", + 2031: "all_addon", + }.get(year, "custom"), + "transfer.start_year": 2035 if year == 2034 else 2026, + "transfer.bill_share": SHARE, + "transfer.oasdi_rate": ADDON_RATE, + } + for path, value in values.items(): + node = p + for key in path.split("."): + node = getattr(node, key) + node.update(period=str(year), value=value) + return parameters + + self.modify_parameters(modify) + + +# Each row is a separate tax unit. Derived MAGI is deliberately used instead +# of entering niit_magi, and dependent income must not join the filer's bases. +ROWS = [ + dict(rental=-10_000, total=20_000), + dict(rental=20_000, total=-10_000), + dict(rental=20_000, total=20_000, agi=230_000), + dict(total=30_000, passive=10_000, already_se=10_000), + dict(total=30_000, passive=10_000, already_se=15_000, overlap=5_000), + dict(rental=20_000, total=30_000, fica=10_000, nii_fica=5_000), + dict(total=100_000, draw=80_000, draw_base=9_235), + dict(total=100_000, draw=80_000, draw_base=9_235, hi=True), + dict(rental=-5_000, total=20_000, nol=15_000), + dict(rental=20_000, total=20_000, agi=230_000, dependent=True), + dict( + rental=15_000, total=20_000, agi=180_000, estate_adjustment=20_000, feie=10_000 + ), + dict(rental=5_000, total=5_000, spouse=True), +] + + +def _situation(): + people, tax_units, households = {}, {}, {} + + def annual(value): + return {str(year): value for year in YEARS} + + for i, row in enumerate(ROWS): + head = f"head_{i}" + people[head] = { + "age": annual(45), + "is_tax_unit_head": annual(True), + "rental_income": annual(row.get("rental", 0)), + "s_corp_income": annual(row.get("total", 0) / 2), + "partnership_income": annual(row.get("total", 0) / 2), + "passive_partnership_s_corp_income": annual(row.get("passive", 0)), + "partnership_self_employment_net_earnings": annual( + row.get("already_se", 0) + ), + "s770_passive_se_income": annual(row.get("overlap", 0)), + "s770_active_fica_income": annual(row.get("fica", 0)), + "s770_draw_gross": annual(row.get("draw", 0)), + "s770_draw_oasdi_base": annual(row.get("draw_base", 0)), + "s770_draw_hi_in_effect": annual(row.get("hi", False)), + "estate_income_niit_magi_adjustment": annual( + row.get("estate_adjustment", 0) + ), + } + members = [head] + if row.get("dependent"): + child = f"child_{i}" + members.append(child) + people[child] = { + "age": annual(12), + "is_tax_unit_dependent": annual(True), + "rental_income": annual(1_000_000), + "s_corp_income": annual(1_000_000), + } + if row.get("spouse"): + spouse = f"spouse_{i}" + members.append(spouse) + people[spouse] = { + "age": annual(45), + "is_tax_unit_spouse": annual(True), + "rental_income": annual(7_000), + "s_corp_income": annual(11_000), + } + tax_units[f"tax_unit_{i}"] = { + "members": members, + "filing_status": annual("JOINT" if row.get("spouse") else "SINGLE"), + "adjusted_gross_income": annual(row.get("agi", 250_000)), + "niit_magi_section_911_addition": annual(row.get("feie", 0)), + "s770_niit_nol_deduction": annual(row.get("nol", 0)), + "s770_nii_fica_seca_income": annual(row.get("nii_fica", 0)), + } + households[f"household_{i}"] = {"members": members, "state_code": annual("TX")} + return {"people": people, "tax_units": tax_units, "households": households} + + +@pytest.fixture(scope="module") +def dated_simulation(): + # One full construction for all integration assertions, without mutation + # after calculations begin. This fixture belongs to Rest's remaining group. + return Simulation( + situation=_situation(), reform=DatedSection8Options, start_instant="2025-01-01" + ) + + +@pytest.mark.parametrize("year", YEARS) +def test_dated_registration_components_and_revenue_accounts(dated_simulation, year): + sim = dated_simulation + nii_on = year >= 2026 and year not in (2027, 2033) + active_on = year >= 2026 and year not in (2026, 2033) + addon_rate = 0 if year == 2032 else ADDON_RATE + expected_nii, expected_active, expected_excess = [], [], [] + for row in ROWS: + nii = row.get("rental", 0) + row.get("passive", 0) + active = row.get("total", 0) - row.get("passive", 0) + active -= row.get("already_se", 0) - row.get("overlap", 0) + active -= row.get("fica", 0) + active -= ( + row.get("draw", 0) + if row.get("hi", False) + else row.get("draw_base", 0) / 0.9235 + ) + if row.get("spouse"): + nii += 7_000 + active += 11_000 + if active_on: + nii -= row.get("overlap", 0) + row.get("nii_fica", 0) + if year != 2032: + nii += row.get("nol", 0) + expected_nii.append(nii) + expected_active.append(active) + threshold = 250_000 if row.get("spouse") else 200_000 + magi = ( + row.get("agi", 250_000) + + row.get("estate_adjustment", 0) + + row.get("feie", 0) + ) + expected_excess.append(max(magi - threshold, 0)) + expected_nii = np.asarray(expected_nii) + expected_active = np.asarray(expected_active) + unified = np.minimum( + np.maximum(expected_nii + (expected_active if active_on else 0), 0), + expected_excess, + ) + n = np.minimum(np.maximum(expected_nii, 0), unified) + a = unified - n + existing_tax = EXISTING_RATE * n + addon_tax = addon_rate * n if nii_on else np.zeros_like(n) + active_tax = ACTIVE_RATE * a if active_on else np.zeros_like(a) + np.testing.assert_allclose( + sim.calculate("s770_nii_base", year), expected_nii, atol=0.01 + ) + np.testing.assert_allclose( + sim.calculate("s770_active_business_income", year), expected_active, atol=0.01 + ) + np.testing.assert_allclose( + sim.calculate("s770_nii_taxable_base", year), n, atol=0.01 + ) + np.testing.assert_allclose( + sim.calculate("s770_active_taxable_base", year), a, atol=0.01 + ) + for variable, expected in ( + ("s770_nii_addon_tax", addon_tax), + ("s770_active_business_income_tax", active_tax), + ("net_investment_income_tax", existing_tax + addon_tax + active_tax), + ): + actual = sim.calculate(variable, year) + np.testing.assert_allclose(actual, expected, atol=0.01) + assert np.all(actual >= 0) + if year == 2029: + shares = (SHARE,) * 3 + elif year == 2030: + shares = (ADDON_RATE / ACTIVE_RATE,) * 3 + elif year == 2031: + shares = (0, 1, SHARE) + else: + shares = (0, SHARE, SHARE) + if year in (2025, 2033, 2034): + shares = (0, 0, 0) + revenues = [] + for variable, share, tax in zip( + ( + "s770_existing_niit_revenue_oasdi", + "s770_nii_addon_revenue_oasdi", + "s770_active_business_income_revenue_oasdi", + ), + shares, + (existing_tax, addon_tax, active_tax), + ): + revenue = sim.calculate(variable, year) + np.testing.assert_allclose(revenue, share * tax, atol=0.01) + revenues.append(revenue) + np.testing.assert_allclose( + sim.calculate("s770_revenue_oasdi", year), sum(revenues), atol=0.01 + ) diff --git a/policyengine_us/variables/gov/contrib/congress/s770/s770_active_business_income.py b/policyengine_us/variables/gov/contrib/congress/s770/s770_active_business_income.py new file mode 100644 index 00000000000..3e33405ff62 --- /dev/null +++ b/policyengine_us/variables/gov/contrib/congress/s770/s770_active_business_income.py @@ -0,0 +1,50 @@ +from policyengine_us.model_api import * +from policyengine_us.reforms.congress.s770.section_8 import active_residual + + +class s770_active_business_income(Variable): + value_type = float + entity = TaxUnit + label = "S.770 signed active business income outside FICA and SECA" + unit = USD + definition_period = YEAR + reference = "https://www.congress.gov/bill/119th-congress/senate-bill/770/text" + documentation = ( + "Signed head/spouse pass-through residual: partnership_s_corp_income " + "(s_corp_income + partnership_income), less its passive subset, " + "already-SE active subset, and amounts actually subject to FICA/SECA " + "under the owners-draw integration hooks. Passive and already-SE " + "subsets must be disjoint unless s770_passive_se_income supplies their " + "intersection. Nonpassive status is only a material-participation " + "proxy; entity-level netting and legal limited-partner classifications " + "are not identified. Certified unrepaired data have zero S-corp income, " + "combined partnership income carrying passive mass, and no passive " + "subset: this overstates the active base and understates NII until " + "Microcosm #1251/#1261 repairs appear in the scoring artifact." + ) + + def formula(tax_unit, period, parameters): + person = tax_unit.members + p = parameters(period).gov.irs + q = 1 - p.ald.misc.employer_share * ( + p.self_employment.rate.social_security + p.self_employment.rate.medicare + ) + if q <= 0: + raise ValueError("S.770 owners-draw netting factor must be positive") + # M7: OASDI-only draws exclude only profits actually taxed after cap + # room. Uncapped HI draws exclude all actual gross draw, never planned D. + draw_exclusion = where( + person("s770_draw_hi_in_effect", period), + person("s770_draw_gross", period), + person("s770_draw_oasdi_base", period) / q, + ) + active_se = person("partnership_self_employment_net_earnings", period) - person( + "s770_passive_se_income", period + ) + residual = active_residual( + person("partnership_s_corp_income", period), + person("passive_partnership_s_corp_income", period), + active_se, + draw_exclusion + person("s770_active_fica_income", period), + ) + return tax_unit.sum(~person("is_tax_unit_dependent", period) * residual) diff --git a/policyengine_us/variables/gov/contrib/congress/s770/s770_active_business_income_revenue_oasdi.py b/policyengine_us/variables/gov/contrib/congress/s770/s770_active_business_income_revenue_oasdi.py new file mode 100644 index 00000000000..65772de73ed --- /dev/null +++ b/policyengine_us/variables/gov/contrib/congress/s770/s770_active_business_income_revenue_oasdi.py @@ -0,0 +1,21 @@ +from policyengine_us.model_api import * +from policyengine_us.reforms.congress.s770.section_8 import ( + enabled, + allocate_bases, + revenue_shares, +) + + +class s770_active_business_income_revenue_oasdi(Variable): + value_type = float + entity = TaxUnit + label = "OASDI revenue from S.770 active business tax" + documentation = "OASDI source accounting alongside tob_revenue_oasdi. Custom mode applies the component share; combined_rate_share applies the OACT combined-rate share; all_addon sends all NII add-on points; bill applies 62 percent to all resulting NIIT including the separately reported existing NIIT revenue. Transfer timing is independent of tax timing." + unit = USD + definition_period = YEAR + reference = "https://www.congress.gov/bill/119th-congress/senate-bill/770/text" + + def formula(tax_unit, period, parameters): + return revenue_shares(parameters, period)[2] * tax_unit( + "s770_active_business_income_tax", period + ) diff --git a/policyengine_us/variables/gov/contrib/congress/s770/s770_active_business_income_tax.py b/policyengine_us/variables/gov/contrib/congress/s770/s770_active_business_income_tax.py new file mode 100644 index 00000000000..3fc6d0bfc8b --- /dev/null +++ b/policyengine_us/variables/gov/contrib/congress/s770/s770_active_business_income_tax.py @@ -0,0 +1,22 @@ +from policyengine_us.model_api import * +from policyengine_us.reforms.congress.s770.section_8 import ( + enabled, + allocate_bases, + revenue_shares, +) + + +class s770_active_business_income_tax(Variable): + value_type = float + entity = TaxUnit + label = "S.770 tax on additional active business income" + documentation = "This component is included in net_investment_income_tax when the structural reform is enabled. Missing passive pass-through data understate NII and overstate active income until the Microcosm entity/passive field repairs are present. See the reform README for statutory category and NOL limitations." + unit = USD + definition_period = YEAR + reference = "https://www.congress.gov/bill/119th-congress/senate-bill/770/text" + + def formula(tax_unit, period, parameters): + p = parameters(period).gov.contrib.congress.s770.section_8.active_business + if not enabled(p, period): + return 0 + return p.rate * tax_unit("s770_active_taxable_base", period) diff --git a/policyengine_us/variables/gov/contrib/congress/s770/s770_active_fica_income.py b/policyengine_us/variables/gov/contrib/congress/s770/s770_active_fica_income.py new file mode 100644 index 00000000000..cf3a02d7501 --- /dev/null +++ b/policyengine_us/variables/gov/contrib/congress/s770/s770_active_fica_income.py @@ -0,0 +1,11 @@ +from policyengine_us.model_api import * + + +class s770_active_fica_income(Variable): + value_type = float + entity = Person + label = "FICA covered income included in active entity totals" + documentation = "Validated FICA covered amount already inside the nonpassive entity total, disjoint from the already-SE subset and draw exclusion. Wages paid separately are not in distributive entity income and must not be subtracted a second time." + unit = USD + definition_period = YEAR + reference = "https://www.congress.gov/bill/119th-congress/senate-bill/770/text" diff --git a/policyengine_us/variables/gov/contrib/congress/s770/s770_active_taxable_base.py b/policyengine_us/variables/gov/contrib/congress/s770/s770_active_taxable_base.py new file mode 100644 index 00000000000..ce21db125e6 --- /dev/null +++ b/policyengine_us/variables/gov/contrib/congress/s770/s770_active_taxable_base.py @@ -0,0 +1,32 @@ +from policyengine_us.model_api import * +from policyengine_us.reforms.congress.s770.section_8 import ( + enabled, + allocate_bases, + revenue_shares, +) + + +class s770_active_taxable_base(Variable): + value_type = float + entity = TaxUnit + label = "S.770 active allocation of the unified taxable base" + documentation = "Net signed NII and active income before clipping to the MAGI excess, then allocate to NII first independently of rates. Both allocated bases are nonnegative. The existing NIIT allocation must change with signed active losses. This component is included in net_investment_income_tax when the structural reform is enabled. Missing passive pass-through data understate NII and overstate active income until the Microcosm entity/passive field repairs are present. See the reform README for statutory category and NOL limitations." + unit = USD + definition_period = YEAR + reference = "https://www.congress.gov/bill/119th-congress/senate-bill/770/text" + + def formula(tax_unit, period, parameters): + p = parameters(period) + options = p.gov.contrib.congress.s770.section_8 + if not enabled(options.active_business, period): + return 0 + niit = p.gov.irs.investment.net_investment_income_tax + excess = ( + tax_unit("niit_magi", period) + - niit.threshold[tax_unit("filing_status", period)] + ) + return allocate_bases( + tax_unit("s770_nii_base", period), + tax_unit("s770_active_business_income", period), + excess, + )[1] diff --git a/policyengine_us/variables/gov/contrib/congress/s770/s770_draw_gross.py b/policyengine_us/variables/gov/contrib/congress/s770/s770_draw_gross.py new file mode 100644 index 00000000000..0477fccf911 --- /dev/null +++ b/policyengine_us/variables/gov/contrib/congress/s770/s770_draw_gross.py @@ -0,0 +1,11 @@ +from policyengine_us.model_api import * + + +class s770_draw_gross(Variable): + value_type = float + entity = Person + label = "Actual gross owners draw for S.770 interaction" + documentation = "Integration hook for actual, floor-accepted owners draw under a future SECA reform. Supply actual gross draw, not planned or phase-in-rejected profits. This input classifies income only and does not itself impose SECA." + unit = USD + definition_period = YEAR + reference = "https://www.congress.gov/bill/119th-congress/senate-bill/770/text" diff --git a/policyengine_us/variables/gov/contrib/congress/s770/s770_draw_hi_in_effect.py b/policyengine_us/variables/gov/contrib/congress/s770/s770_draw_hi_in_effect.py new file mode 100644 index 00000000000..9737350c692 --- /dev/null +++ b/policyengine_us/variables/gov/contrib/congress/s770/s770_draw_hi_in_effect.py @@ -0,0 +1,10 @@ +from policyengine_us.model_api import * + + +class s770_draw_hi_in_effect(Variable): + value_type = bool + entity = Person + label = "Actual owners draw is subject to uncapped SECA HI" + documentation = "Integration hook: true only where the actual gross draw supplied in s770_draw_gross is subject to uncapped SECA HI. Exclude the full gross draw in that case; otherwise exclude only the gross equivalent of its capped OASDI base. Undrawn and floor-rejected amounts stay in active income." + definition_period = YEAR + reference = "https://www.congress.gov/bill/119th-congress/senate-bill/770/text" diff --git a/policyengine_us/variables/gov/contrib/congress/s770/s770_draw_oasdi_base.py b/policyengine_us/variables/gov/contrib/congress/s770/s770_draw_oasdi_base.py new file mode 100644 index 00000000000..a7fd63573f7 --- /dev/null +++ b/policyengine_us/variables/gov/contrib/congress/s770/s770_draw_oasdi_base.py @@ -0,0 +1,11 @@ +from policyengine_us.model_api import * + + +class s770_draw_oasdi_base(Variable): + value_type = float + entity = Person + label = "Owners draw net earnings actually taxed by OASDI" + documentation = "Integration hook for adjusted owners draw after wages, existing SE earnings and the applicable cap room. With draw HI off, only this amount divided by the SE netting factor is excluded from active income; above-room profits stay in the active base. This input does not itself impose SECA." + unit = USD + definition_period = YEAR + reference = "https://www.congress.gov/bill/119th-congress/senate-bill/770/text" diff --git a/policyengine_us/variables/gov/contrib/congress/s770/s770_existing_niit_revenue_oasdi.py b/policyengine_us/variables/gov/contrib/congress/s770/s770_existing_niit_revenue_oasdi.py new file mode 100644 index 00000000000..c7ca17c6069 --- /dev/null +++ b/policyengine_us/variables/gov/contrib/congress/s770/s770_existing_niit_revenue_oasdi.py @@ -0,0 +1,21 @@ +from policyengine_us.model_api import * +from policyengine_us.reforms.congress.s770.section_8 import ( + enabled, + allocate_bases, + revenue_shares, +) + + +class s770_existing_niit_revenue_oasdi(Variable): + value_type = float + entity = TaxUnit + label = "OASDI revenue from S.770 allocation of existing NIIT" + documentation = "OASDI source accounting alongside tob_revenue_oasdi. Custom mode applies the component share; combined_rate_share applies the OACT combined-rate share; all_addon sends all NII add-on points; bill applies 62 percent to all resulting NIIT including the separately reported existing NIIT revenue. Transfer timing is independent of tax timing." + unit = USD + definition_period = YEAR + reference = "https://www.congress.gov/bill/119th-congress/senate-bill/770/text" + + def formula(tax_unit, period, parameters): + return revenue_shares(parameters, period)[0] * tax_unit( + "s770_existing_niit_tax", period + ) diff --git a/policyengine_us/variables/gov/contrib/congress/s770/s770_existing_niit_tax.py b/policyengine_us/variables/gov/contrib/congress/s770/s770_existing_niit_tax.py new file mode 100644 index 00000000000..7559062f96d --- /dev/null +++ b/policyengine_us/variables/gov/contrib/congress/s770/s770_existing_niit_tax.py @@ -0,0 +1,20 @@ +from policyengine_us.model_api import * +from policyengine_us.reforms.congress.s770.section_8 import ( + enabled, + allocate_bases, + revenue_shares, +) + + +class s770_existing_niit_tax(Variable): + value_type = float + entity = TaxUnit + label = "Existing NIIT rate on the S.770 NII allocation" + documentation = "This component is included in net_investment_income_tax when the structural reform is enabled. Missing passive pass-through data understate NII and overstate active income until the Microcosm entity/passive field repairs are present. See the reform README for statutory category and NOL limitations." + unit = USD + definition_period = YEAR + reference = "https://www.congress.gov/bill/119th-congress/senate-bill/770/text" + + def formula(tax_unit, period, parameters): + rate = parameters(period).gov.irs.investment.net_investment_income_tax.rate + return rate * tax_unit("s770_nii_taxable_base", period) diff --git a/policyengine_us/variables/gov/contrib/congress/s770/s770_nii_addon_revenue_oasdi.py b/policyengine_us/variables/gov/contrib/congress/s770/s770_nii_addon_revenue_oasdi.py new file mode 100644 index 00000000000..842392e6315 --- /dev/null +++ b/policyengine_us/variables/gov/contrib/congress/s770/s770_nii_addon_revenue_oasdi.py @@ -0,0 +1,21 @@ +from policyengine_us.model_api import * +from policyengine_us.reforms.congress.s770.section_8 import ( + enabled, + allocate_bases, + revenue_shares, +) + + +class s770_nii_addon_revenue_oasdi(Variable): + value_type = float + entity = TaxUnit + label = "OASDI revenue from S.770 NII add-on" + documentation = "OASDI source accounting alongside tob_revenue_oasdi. Custom mode applies the component share; combined_rate_share applies the OACT combined-rate share; all_addon sends all NII add-on points; bill applies 62 percent to all resulting NIIT including the separately reported existing NIIT revenue. Transfer timing is independent of tax timing." + unit = USD + definition_period = YEAR + reference = "https://www.congress.gov/bill/119th-congress/senate-bill/770/text" + + def formula(tax_unit, period, parameters): + return revenue_shares(parameters, period)[1] * tax_unit( + "s770_nii_addon_tax", period + ) diff --git a/policyengine_us/variables/gov/contrib/congress/s770/s770_nii_addon_tax.py b/policyengine_us/variables/gov/contrib/congress/s770/s770_nii_addon_tax.py new file mode 100644 index 00000000000..f864c02000a --- /dev/null +++ b/policyengine_us/variables/gov/contrib/congress/s770/s770_nii_addon_tax.py @@ -0,0 +1,22 @@ +from policyengine_us.model_api import * +from policyengine_us.reforms.congress.s770.section_8 import ( + enabled, + allocate_bases, + revenue_shares, +) + + +class s770_nii_addon_tax(Variable): + value_type = float + entity = TaxUnit + label = "S.770 additional tax on net investment income" + documentation = "This component is included in net_investment_income_tax when the structural reform is enabled. Missing passive pass-through data understate NII and overstate active income until the Microcosm entity/passive field repairs are present. See the reform README for statutory category and NOL limitations." + unit = USD + definition_period = YEAR + reference = "https://www.congress.gov/bill/119th-congress/senate-bill/770/text" + + def formula(tax_unit, period, parameters): + p = parameters(period).gov.contrib.congress.s770.section_8.nii_addon + if not enabled(p, period): + return 0 + return p.rate * tax_unit("s770_nii_taxable_base", period) diff --git a/policyengine_us/variables/gov/contrib/congress/s770/s770_nii_base.py b/policyengine_us/variables/gov/contrib/congress/s770/s770_nii_base.py new file mode 100644 index 00000000000..c919fc3d487 --- /dev/null +++ b/policyengine_us/variables/gov/contrib/congress/s770/s770_nii_base.py @@ -0,0 +1,33 @@ +from policyengine_us.model_api import * +from policyengine_us.reforms.congress.s770.section_8 import enabled + + +class s770_nii_base(Variable): + value_type = float + entity = TaxUnit + label = "S.770 signed NII with selected source adjustments" + unit = USD + definition_period = YEAR + reference = "https://www.congress.gov/bill/119th-congress/senate-bill/770/text" + documentation = ( + "Existing signed NII, adjusted when the active option applies for " + "validated NOL denial and items actually subject to FICA/SECA. The " + "NOL hook changes broadened NII itself before unified allocation, " + "not a separately floored active bucket. No federal section 172 " + "deduction is automatically modeled. Literal section 8 also broadens " + "ordinary-course investment income and business-property gains; " + "their full classification is unsupported here. Missing passive " + "pass-through income in unrepaired data understates this NII source " + "while overstating the active residual; see the reform documentation." + ) + + def formula(tax_unit, period, parameters): + nii = tax_unit("net_investment_income", period) + p = parameters(period).gov.contrib.congress.s770.section_8.active_business + if not enabled(p, period): + return nii + overlap = tax_unit_non_dep_add(tax_unit, period, ["s770_passive_se_income"]) + nii = nii - overlap - tax_unit("s770_nii_fica_seca_income", period) + if p.deny_nol: + nii = nii + tax_unit("s770_niit_nol_deduction", period) + return nii diff --git a/policyengine_us/variables/gov/contrib/congress/s770/s770_nii_fica_seca_income.py b/policyengine_us/variables/gov/contrib/congress/s770/s770_nii_fica_seca_income.py new file mode 100644 index 00000000000..75f86765d19 --- /dev/null +++ b/policyengine_us/variables/gov/contrib/congress/s770/s770_nii_fica_seca_income.py @@ -0,0 +1,11 @@ +from policyengine_us.model_api import * + + +class s770_nii_fica_seca_income(Variable): + value_type = float + entity = TaxUnit + label = "Already FICA or SECA covered income in supplied NII" + documentation = "Validated amount already included in NII and taxed under section 1401 or 3101, other than the passive/already-SE intersection supplied separately. The active option removes it from broadened NII. Requires source classification; wages are not automatically subtracted from unrelated entity income." + unit = USD + definition_period = YEAR + reference = "https://www.congress.gov/bill/119th-congress/senate-bill/770/text" diff --git a/policyengine_us/variables/gov/contrib/congress/s770/s770_nii_taxable_base.py b/policyengine_us/variables/gov/contrib/congress/s770/s770_nii_taxable_base.py new file mode 100644 index 00000000000..73784487653 --- /dev/null +++ b/policyengine_us/variables/gov/contrib/congress/s770/s770_nii_taxable_base.py @@ -0,0 +1,31 @@ +from policyengine_us.model_api import * +from policyengine_us.reforms.congress.s770.section_8 import ( + enabled, + allocate_bases, + revenue_shares, +) + + +class s770_nii_taxable_base(Variable): + value_type = float + entity = TaxUnit + label = "S.770 NII allocation of the unified taxable base" + documentation = "Net signed NII and active income before clipping to the MAGI excess, then allocate to NII first independently of rates. Both allocated bases are nonnegative. The existing NIIT allocation must change with signed active losses. This component is included in net_investment_income_tax when the structural reform is enabled. Missing passive pass-through data understate NII and overstate active income until the Microcosm entity/passive field repairs are present. See the reform README for statutory category and NOL limitations." + unit = USD + definition_period = YEAR + reference = "https://www.congress.gov/bill/119th-congress/senate-bill/770/text" + + def formula(tax_unit, period, parameters): + p = parameters(period) + niit = p.gov.irs.investment.net_investment_income_tax + options = p.gov.contrib.congress.s770.section_8 + excess = ( + tax_unit("niit_magi", period) + - niit.threshold[tax_unit("filing_status", period)] + ) + active = ( + tax_unit("s770_active_business_income", period) + if enabled(options.active_business, period) + else 0 + ) + return allocate_bases(tax_unit("s770_nii_base", period), active, excess)[0] diff --git a/policyengine_us/variables/gov/contrib/congress/s770/s770_niit_nol_deduction.py b/policyengine_us/variables/gov/contrib/congress/s770/s770_niit_nol_deduction.py new file mode 100644 index 00000000000..0a67969cfcb --- /dev/null +++ b/policyengine_us/variables/gov/contrib/congress/s770/s770_niit_nol_deduction.py @@ -0,0 +1,11 @@ +from policyengine_us.model_api import * + + +class s770_niit_nol_deduction(Variable): + value_type = float + entity = TaxUnit + label = "NOL deduction already included in supplied NII" + documentation = "Validated section 172 deduction already subtracted from net_investment_income. The active option adds it back to broadened NII if deny_nol is true. Supply only an amount actually deducted from the NII input, not an AGI deduction or a signed business loss. Baseline source formulas contain no federal NOL deduction; default zero does not assert complete NOL coverage." + unit = USD + definition_period = YEAR + reference = "https://www.congress.gov/bill/119th-congress/senate-bill/770/text" diff --git a/policyengine_us/variables/gov/contrib/congress/s770/s770_passive_se_income.py b/policyengine_us/variables/gov/contrib/congress/s770/s770_passive_se_income.py new file mode 100644 index 00000000000..9f638b79ae2 --- /dev/null +++ b/policyengine_us/variables/gov/contrib/congress/s770/s770_passive_se_income.py @@ -0,0 +1,11 @@ +from policyengine_us.model_api import * + + +class s770_passive_se_income(Variable): + value_type = float + entity = Person + label = "Intersection of passive and already SE covered entity income" + documentation = "Intersection already included in both passive_partnership_s_corp_income and partnership_self_employment_net_earnings. Default zero assumes disjoint subsets; supply the validated intersection when they overlap. Active inclusion removes it from NII and subtracts only the active portion of already-SE income from the active residual." + unit = USD + definition_period = YEAR + reference = "https://www.congress.gov/bill/119th-congress/senate-bill/770/text" diff --git a/policyengine_us/variables/gov/contrib/congress/s770/s770_revenue_oasdi.py b/policyengine_us/variables/gov/contrib/congress/s770/s770_revenue_oasdi.py new file mode 100644 index 00000000000..6812581be1f --- /dev/null +++ b/policyengine_us/variables/gov/contrib/congress/s770/s770_revenue_oasdi.py @@ -0,0 +1,16 @@ +from policyengine_us.model_api import * + + +class s770_revenue_oasdi(Variable): + value_type = float + entity = TaxUnit + label = "OASDI revenue from S.770 section 8 options" + documentation = "Sum each TaxUnit once, alongside tob_revenue_oasdi; includes the existing NIIT transfer only where the selected mode transfers it. Liability and trust-fund allocation equivalence are separate claims." + unit = USD + definition_period = YEAR + reference = "https://www.congress.gov/bill/119th-congress/senate-bill/770/text" + adds = [ + "s770_existing_niit_revenue_oasdi", + "s770_nii_addon_revenue_oasdi", + "s770_active_business_income_revenue_oasdi", + ] From 64a2fcc3879b364e2d8bba1136a7187c44143308 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 02:47:28 -0400 Subject: [PATCH 2/9] Use supported transfer mode parameters and cover independent rate allocations --- .../s770/section_8/transfer/mode.yaml | 5 +- .../s770/section_8/transfer/oasdi_rate.yaml | 2 + .../reforms/congress/s770/README.md | 20 ++++--- .../reforms/congress/s770/section_8.py | 4 ++ .../contrib/congress/s770/section_8.yaml | 59 +++++++++++++++++-- .../tests/reforms/test_s770_section_8.py | 15 ++--- 6 files changed, 82 insertions(+), 23 deletions(-) diff --git a/policyengine_us/parameters/gov/contrib/congress/s770/section_8/transfer/mode.yaml b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/transfer/mode.yaml index 51ee7cd1757..f11d9f5529d 100644 --- a/policyengine_us/parameters/gov/contrib/congress/s770/section_8/transfer/mode.yaml +++ b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/transfer/mode.yaml @@ -1,13 +1,14 @@ -description: 'OASDI transfer convention: custom uses each incremental part share; combined_rate_share +description: 'OASDI transfer convention: 0=custom, 1=combined_rate_share, 2=all_addon, 3=bill. Custom uses each incremental part share; combined_rate_share allocates the OASDI rate over each combined rate; all_addon sends every NII add-on point to OASDI and uses the active share; bill sends the bill share of the entire resulting section 1411 tax, including existing NIIT. This selects transfers, not full statutory category coverage.' values: - '0000-01-01': custom + '0000-01-01': 0 metadata: label: S.770 OASDI transfer mode period: year + unit: int reference: - title: S.770 (119th Congress), sections 8 and 9 href: https://www.congress.gov/bill/119th-congress/senate-bill/770/text diff --git a/policyengine_us/parameters/gov/contrib/congress/s770/section_8/transfer/oasdi_rate.yaml b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/transfer/oasdi_rate.yaml index 25251305c5d..79c6581dd32 100644 --- a/policyengine_us/parameters/gov/contrib/congress/s770/section_8/transfer/oasdi_rate.yaml +++ b/policyengine_us/parameters/gov/contrib/congress/s770/section_8/transfer/oasdi_rate.yaml @@ -9,4 +9,6 @@ metadata: reference: - title: S.770 (119th Congress), sections 8 and 9 href: https://www.congress.gov/bill/119th-congress/senate-bill/770/text + - title: OACT 2025 coverage provisions, F8 + href: https://www.ssa.gov/OACT/solvency/provisions_tr2025/coverage.html unit: /1 diff --git a/policyengine_us/reforms/congress/s770/README.md b/policyengine_us/reforms/congress/s770/README.md index b054965bc7d..a03e0c21fa9 100644 --- a/policyengine_us/reforms/congress/s770/README.md +++ b/policyengine_us/reforms/congress/s770/README.md @@ -110,13 +110,19 @@ Transfers are separate from liability. `transfer.mode` selects: | Mode | Existing NIIT | NII add-on | Active tax | | --- | --- | --- | --- | -| `custom` (default) | 0 | `nii_addon.oasdi_share` | `active_business.oasdi_share` | -| `combined_rate_share` | OASDI rate / combined NII rate | Same combined share | OASDI rate / active rate | -| `all_addon` | 0 | 100% | `active_business.oasdi_share` | -| `bill` | `transfer.bill_share` | Same bill share | Same bill share | - -The OACT-style 12.4/16.2 convention is approximately 76.5432 percent, distinct -from the bill's 62 percent. A combined-rate share requires an OASDI rate no +| `0` = `custom` (default) | 0 | `nii_addon.oasdi_share` | `active_business.oasdi_share` | +| `1` = `combined_rate_share` | OASDI rate / combined NII rate | Same combined share | OASDI rate / active rate | +| `2` = `all_addon` | 0 | 100% | `active_business.oasdi_share` | +| `3` = `bill` | `transfer.bill_share` | Same bill share | Same bill share | + +The mode parameter uses integer codes because core does not support string +parameter scalars. + +The [OACT 2025 F8 description](https://www.ssa.gov/OACT/solvency/provisions_tr2025/coverage.html) +allocates the active tax attributable to 12.4 of its 16.2 percentage points to +OASDI. That convention is approximately 76.5432 percent, distinct from the +bill's 62 percent. Here it is also available for the combined NII rate as a +custom transfer convention. A combined-rate share requires an OASDI rate no larger than each positive tax rate to which it applies; a zero tax has zero transfer. When the NII add-on is off or its rate is zero, this custom mode transfers no existing NIIT, while the active transfer remains independent. diff --git a/policyengine_us/reforms/congress/s770/section_8.py b/policyengine_us/reforms/congress/s770/section_8.py index 1cd588b19d1..1334c1ed74c 100644 --- a/policyengine_us/reforms/congress/s770/section_8.py +++ b/policyengine_us/reforms/congress/s770/section_8.py @@ -32,6 +32,10 @@ def transfer_shares( oasdi_rate, ): """Shares of existing NIIT, the add-on, and active tax, respectively.""" + # Core parameters accept numbers, booleans and lists, not string scalars. + mode = {0: "custom", 1: "combined_rate_share", 2: "all_addon", 3: "bill"}.get( + mode, mode + ) if mode == "custom": return 0, nii_share, active_share if mode == "bill": diff --git a/policyengine_us/tests/policy/contrib/congress/s770/section_8.yaml b/policyengine_us/tests/policy/contrib/congress/s770/section_8.yaml index 02a25ff254d..ab7e5f8e24d 100644 --- a/policyengine_us/tests/policy/contrib/congress/s770/section_8.yaml +++ b/policyengine_us/tests/policy/contrib/congress/s770/section_8.yaml @@ -338,7 +338,7 @@ s770_active_business_income_tax: 0 net_investment_income_tax: 0 -- name: Both options respect the bill start year +- name: Both options respect the illustrative default start year period: 2025 absolute_error_margin: 0.01 reforms: policyengine_us.reforms.congress.s770.section_8.section_8 @@ -396,7 +396,7 @@ input: gov.contrib.congress.s770.section_8.nii_addon.in_effect: true gov.contrib.congress.s770.section_8.active_business.in_effect: true - gov.contrib.congress.s770.section_8.transfer.mode: bill + gov.contrib.congress.s770.section_8.transfer.mode: 3 taxable_interest_income: 100_000 s_corp_income: 200_000 niit_magi: 400_000 @@ -415,7 +415,7 @@ input: gov.contrib.congress.s770.section_8.nii_addon.in_effect: true gov.contrib.congress.s770.section_8.active_business.in_effect: true - gov.contrib.congress.s770.section_8.transfer.mode: combined_rate_share + gov.contrib.congress.s770.section_8.transfer.mode: 1 taxable_interest_income: 100_000 s_corp_income: 200_000 niit_magi: 400_000 @@ -434,7 +434,7 @@ input: gov.contrib.congress.s770.section_8.nii_addon.in_effect: true gov.contrib.congress.s770.section_8.active_business.in_effect: true - gov.contrib.congress.s770.section_8.transfer.mode: all_addon + gov.contrib.congress.s770.section_8.transfer.mode: 2 taxable_interest_income: 100_000 s_corp_income: 200_000 niit_magi: 400_000 @@ -445,6 +445,55 @@ s770_active_business_income_revenue_oasdi: 10_044 s770_revenue_oasdi: 22_444 +- name: Combined rate transfer works with the active option independently enabled + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: false + gov.contrib.congress.s770.section_8.active_business.in_effect: true + gov.contrib.congress.s770.section_8.transfer.mode: 1 + taxable_interest_income: 100_000 + s_corp_income: 200_000 + niit_magi: 400_000 + filing_status: SINGLE + output: + s770_nii_taxable_base: 100_000 + s770_active_taxable_base: 100_000 + s770_nii_addon_tax: 0 + s770_active_business_income_tax: 16_200 + net_investment_income_tax: 20_000 + # No NII add-on means no transfer of the existing 3.8 percent tax. + s770_existing_niit_revenue_oasdi: 0 + s770_nii_addon_revenue_oasdi: 0 + s770_active_business_income_revenue_oasdi: 12_400 + s770_revenue_oasdi: 12_400 + +- name: Combined rate transfer with zero addon preserves NII threshold consumption + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.nii_addon.rate: 0 + gov.contrib.congress.s770.section_8.active_business.in_effect: true + gov.contrib.congress.s770.section_8.transfer.mode: 1 + taxable_interest_income: 20_000 + s_corp_income: 20_000 + niit_magi: 230_000 + filing_status: SINGLE + output: + # NII still takes 20,000 of the shared 30,000 MAGI excess first. + s770_nii_taxable_base: 20_000 + s770_active_taxable_base: 10_000 + s770_nii_addon_tax: 0 + s770_active_business_income_tax: 1_620 + net_investment_income_tax: 2_380 + s770_existing_niit_revenue_oasdi: 0 + s770_nii_addon_revenue_oasdi: 0 + s770_active_business_income_revenue_oasdi: 1_240 + s770_revenue_oasdi: 1_240 + - name: Custom transfer shares remain independently configurable period: 2026 absolute_error_margin: 0.01 @@ -471,7 +520,7 @@ input: gov.contrib.congress.s770.section_8.nii_addon.in_effect: true gov.contrib.congress.s770.section_8.active_business.in_effect: true - gov.contrib.congress.s770.section_8.transfer.mode: bill + gov.contrib.congress.s770.section_8.transfer.mode: 3 gov.contrib.congress.s770.section_8.transfer.start_year: 2027 taxable_interest_income: 100_000 s_corp_income: 200_000 diff --git a/policyengine_us/tests/reforms/test_s770_section_8.py b/policyengine_us/tests/reforms/test_s770_section_8.py index a4e7bc44c88..97f77f785a3 100644 --- a/policyengine_us/tests/reforms/test_s770_section_8.py +++ b/policyengine_us/tests/reforms/test_s770_section_8.py @@ -4,6 +4,8 @@ the model. One shared, read-only simulation then checks registration, changing switches/start years, entity aggregation and revenue accounting across years; YAML cannot express those dated parameter changes. No population data are used. +Direct-NII YAML cases cover the NOL-denial hook separately from these source +aggregation checks. Unified-liability equivalence assumes identical signed sources, exclusions, NOL treatment, MAGI, thresholds and timing, with existing rate + add-on rate = @@ -190,12 +192,11 @@ def modify(parameters): "active_business.rate": ACTIVE_RATE, "nii_addon.oasdi_share": SHARE, "active_business.oasdi_share": SHARE, - "active_business.deny_nol": year != 2032, "transfer.mode": { - 2029: "bill", - 2030: "combined_rate_share", - 2031: "all_addon", - }.get(year, "custom"), + 2029: 3, + 2030: 1, + 2031: 2, + }.get(year, 0), "transfer.start_year": 2035 if year == 2034 else 2026, "transfer.bill_share": SHARE, "transfer.oasdi_rate": ADDON_RATE, @@ -221,7 +222,6 @@ def modify(parameters): dict(rental=20_000, total=30_000, fica=10_000, nii_fica=5_000), dict(total=100_000, draw=80_000, draw_base=9_235), dict(total=100_000, draw=80_000, draw_base=9_235, hi=True), - dict(rental=-5_000, total=20_000, nol=15_000), dict(rental=20_000, total=20_000, agi=230_000, dependent=True), dict( rental=15_000, total=20_000, agi=180_000, estate_adjustment=20_000, feie=10_000 @@ -281,7 +281,6 @@ def annual(value): "filing_status": annual("JOINT" if row.get("spouse") else "SINGLE"), "adjusted_gross_income": annual(row.get("agi", 250_000)), "niit_magi_section_911_addition": annual(row.get("feie", 0)), - "s770_niit_nol_deduction": annual(row.get("nol", 0)), "s770_nii_fica_seca_income": annual(row.get("nii_fica", 0)), } households[f"household_{i}"] = {"members": members, "state_code": annual("TX")} @@ -319,8 +318,6 @@ def test_dated_registration_components_and_revenue_accounts(dated_simulation, ye active += 11_000 if active_on: nii -= row.get("overlap", 0) + row.get("nii_fica", 0) - if year != 2032: - nii += row.get("nol", 0) expected_nii.append(nii) expected_active.append(active) threshold = 250_000 if row.get("spouse") else 200_000 From 287293539072c72c6b3c73e656e6a850edef5b7b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 03:36:08 -0400 Subject: [PATCH 3/9] Cover draw netting factors and qualify activation monotonicity --- .../reforms/congress/s770/README.md | 12 +- .../contrib/congress/s770/section_8.yaml | 104 ++++++++++++++++++ .../tests/reforms/test_s770_section_8.py | 9 +- 3 files changed, 121 insertions(+), 4 deletions(-) diff --git a/policyengine_us/reforms/congress/s770/README.md b/policyengine_us/reforms/congress/s770/README.md index a03e0c21fa9..f99545d3063 100644 --- a/policyengine_us/reforms/congress/s770/README.md +++ b/policyengine_us/reforms/congress/s770/README.md @@ -51,7 +51,11 @@ that the available fields implement every statutory category. For `N=−10,000`, `A=20,000`, `X=50,000`, the tax is $1,620. For `N=20,000`, `A=−10,000`, the tax is also $1,620. Active losses can therefore reduce liability relative to the add-on alone ($3,240 in the latter example). Activation is monotone with -nonnegative active income and nonnegative rates, not for arbitrary signed losses. +nonnegative active income, nonnegative rates and fixed signed NII/source rules, +including exclusions and NOL treatment. Changing source exclusions can lower +liability even when the active residual is nonnegative: removing $10,000 of +already-SE passive income from NII with no active residual reduces existing NIIT +from $380 to zero. Arbitrary signed active losses also defeat monotonicity. ## Pass-through classification and SECA/FICA interaction @@ -83,7 +87,11 @@ and do not impose SECA themselves. The supplier must connect actual taxed amount Here `q=1−employer_share*(SE_OASDI_rate+SE_HI_rate)`, currently .9235, using existing parameters. The undrawn phase-in share and floor-rejected planned draw -remain in `A`. No independent owners-draw policy is enabled by these hooks. +remain in `A`. The supplying reform must apply its phase-in and reporting floor +and provide the actual floor-accepted draw and taxed base; an undrawn or +floor-rejected draw supplies zero for both hooks, including when HI is selected. +These hooks do not calculate the draw, phase-in or reporting floor themselves. +No independent owners-draw policy is enabled by these hooks. ## Literal-bill category audit and NOL support diff --git a/policyengine_us/tests/policy/contrib/congress/s770/section_8.yaml b/policyengine_us/tests/policy/contrib/congress/s770/section_8.yaml index ab7e5f8e24d..de1bf45e967 100644 --- a/policyengine_us/tests/policy/contrib/congress/s770/section_8.yaml +++ b/policyengine_us/tests/policy/contrib/congress/s770/section_8.yaml @@ -629,6 +629,110 @@ s770_active_business_income_tax: 32_400 net_investment_income_tax: 32_400 +- name: Draw gross exclusion uses the netting factor from changed SECA rates + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.active_business.in_effect: true + gov.irs.self_employment.rate.social_security: .14 + gov.irs.self_employment.rate.medicare: .06 + s_corp_income: 200_000 + s770_draw_gross: 150_000 + # q = 1 - .5 x (.14 + .06) = .9, so 9,000 / q = 10,000 gross. + s770_draw_oasdi_base: 9_000 + s770_draw_hi_in_effect: false + niit_magi: 400_000 + filing_status: SINGLE + output: + s770_active_business_income: 190_000 + s770_active_taxable_base: 190_000 + s770_active_business_income_tax: 30_780 + net_investment_income_tax: 30_780 + +- name: Undrawn and floor rejected draws supply zero actual classification hooks + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.active_business.in_effect: true + people: + undrawn: + age: 45 + s_corp_income: 200_000 + # The supplying draw reform has a zero phase-in share. + s770_draw_gross: 0 + s770_draw_oasdi_base: 0 + s770_draw_hi_in_effect: false + floor_rejected: + age: 45 + s_corp_income: 200_000 + # The supplying reform rejected a planned $200 draw below its floor. + # It must supply actual accepted draw/base of zero even with HI on. + # These hooks do not calculate the phase-in or reporting floor. + s770_draw_gross: 0 + s770_draw_oasdi_base: 0 + s770_draw_hi_in_effect: true + tax_units: + undrawn: + members: [undrawn] + filing_status: SINGLE + niit_magi: 400_000 + floor_rejected: + members: [floor_rejected] + filing_status: SINGLE + niit_magi: 400_000 + households: + household: + members: [undrawn, floor_rejected] + state_code: TX + output: + s770_active_business_income: [200_000, 200_000] + s770_active_taxable_base: [200_000, 200_000] + s770_active_business_income_tax: [32_400, 32_400] + net_investment_income_tax: [32_400, 32_400] + +- name: Before active inclusion already SE passive income remains in existing NII + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + partnership_income: 10_000 + passive_partnership_s_corp_income: 10_000 + partnership_self_employment_net_earnings: 10_000 + s770_passive_se_income: 10_000 + niit_magi: 250_000 + filing_status: SINGLE + output: + s770_nii_base: 10_000 + s770_active_business_income: 0 + s770_nii_taxable_base: 10_000 + s770_nii_addon_tax: 0 + s770_active_business_income_tax: 0 + net_investment_income_tax: 380 + +- name: Activating the SECA exclusion can lower tax with no signed active loss + period: 2026 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.active_business.in_effect: true + partnership_income: 10_000 + passive_partnership_s_corp_income: 10_000 + partnership_self_employment_net_earnings: 10_000 + s770_passive_se_income: 10_000 + niit_magi: 250_000 + filing_status: SINGLE + output: + # Same household as above: N falls by 10,000, while A stays zero. + # Monotonic activation requires fixed NII/source exclusions as well as A>=0. + s770_nii_base: 0 + s770_active_business_income: 0 + s770_nii_taxable_base: 0 + s770_active_taxable_base: 0 + s770_nii_addon_tax: 0 + s770_active_business_income_tax: 0 + net_investment_income_tax: 0 + - name: Explicit passive SE overlap is removed once from each affected category period: 2026 absolute_error_margin: 0.01 diff --git a/policyengine_us/tests/reforms/test_s770_section_8.py b/policyengine_us/tests/reforms/test_s770_section_8.py index 97f77f785a3..cb4e0a5880b 100644 --- a/policyengine_us/tests/reforms/test_s770_section_8.py +++ b/policyengine_us/tests/reforms/test_s770_section_8.py @@ -11,7 +11,10 @@ NOL treatment, MAGI, thresholds and timing, with existing rate + add-on rate = active rate = 16.2%. It does not imply equal OASDI transfers or full statutory category coverage. Adding a signed active loss can lower total tax, so the -monotonicity property requires a nonnegative active residual. +monotonicity property requires a nonnegative active residual and fixed signed +NII/source rules, including exclusions and NOL treatment, across switches. +Activating an exclusion can lower existing NIIT even with a nonnegative active +residual; YAML pins that separate source-classification counterexample. """ from types import SimpleNamespace @@ -78,9 +81,11 @@ def test_review_counterexamples_and_zero_addon_threshold_allocation(): @SETTINGS @hypothesis.given(nii=SIGNED, active=NONNEGATIVE, excess=NONNEGATIVE) -def test_turning_on_parts_is_monotone_for_nonnegative_active_income( +def test_turning_on_parts_is_monotone_with_fixed_nii_and_nonnegative_active_income( nii, active, excess ): + # The signed NII input is fixed across switches. Applying new SECA/FICA + # source exclusions when the active option starts is outside this property. n, a = allocate_bases(nii, active, excess) n_without_active, _ = allocate_bases(nii, 0, excess) baseline = EXISTING_RATE * n_without_active From 1bb2f9668087b0b8eb4a6ece5e840560cbaf0be4 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 04:04:40 -0400 Subject: [PATCH 4/9] Split section 8 policy tests within the reform memory budget --- ....yaml => s770_active_business_income.yaml} | 238 ------------------ ..._active_business_income_revenue_oasdi.yaml | 69 +++++ .../congress/s770/s770_nii_taxable_base.yaml | 94 +++++++ .../congress/s770/s770_revenue_oasdi.yaml | 75 ++++++ 4 files changed, 238 insertions(+), 238 deletions(-) rename policyengine_us/tests/policy/contrib/congress/s770/{section_8.yaml => s770_active_business_income.yaml} (69%) create mode 100644 policyengine_us/tests/policy/contrib/congress/s770/s770_active_business_income_revenue_oasdi.yaml create mode 100644 policyengine_us/tests/policy/contrib/congress/s770/s770_nii_taxable_base.yaml create mode 100644 policyengine_us/tests/policy/contrib/congress/s770/s770_revenue_oasdi.yaml diff --git a/policyengine_us/tests/policy/contrib/congress/s770/section_8.yaml b/policyengine_us/tests/policy/contrib/congress/s770/s770_active_business_income.yaml similarity index 69% rename from policyengine_us/tests/policy/contrib/congress/s770/section_8.yaml rename to policyengine_us/tests/policy/contrib/congress/s770/s770_active_business_income.yaml index de1bf45e967..bfd010914d3 100644 --- a/policyengine_us/tests/policy/contrib/congress/s770/section_8.yaml +++ b/policyengine_us/tests/policy/contrib/congress/s770/s770_active_business_income.yaml @@ -163,45 +163,6 @@ s770_existing_niit_revenue_oasdi: 0 s770_revenue_oasdi: 17_732 -- name: Zero addon rate still reserves NII threshold room - period: 2026 - absolute_error_margin: 0.01 - reforms: policyengine_us.reforms.congress.s770.section_8.section_8 - input: - gov.contrib.congress.s770.section_8.nii_addon.in_effect: true - gov.contrib.congress.s770.section_8.nii_addon.rate: 0 - gov.contrib.congress.s770.section_8.active_business.in_effect: true - taxable_interest_income: 20_000 - s_corp_income: 20_000 - niit_magi: 230_000 - filing_status: SINGLE - output: - s770_nii_taxable_base: 20_000 - s770_active_taxable_base: 10_000 - s770_nii_addon_tax: 0 - s770_active_business_income_tax: 1_620 - # .038 x 20,000 + .162 x 10,000. - net_investment_income_tax: 2_380 - -- name: Zero active rate does not change the NII allocation - period: 2026 - absolute_error_margin: 0.01 - reforms: policyengine_us.reforms.congress.s770.section_8.section_8 - input: - gov.contrib.congress.s770.section_8.nii_addon.in_effect: true - gov.contrib.congress.s770.section_8.active_business.in_effect: true - gov.contrib.congress.s770.section_8.active_business.rate: 0 - taxable_interest_income: 20_000 - s_corp_income: 20_000 - niit_magi: 230_000 - filing_status: SINGLE - output: - s770_nii_taxable_base: 20_000 - s770_active_taxable_base: 10_000 - s770_nii_addon_tax: 2_480 - s770_active_business_income_tax: 0 - net_investment_income_tax: 3_240 - - name: Signed NII loss offsets active income before either bucket is clipped period: 2026 absolute_error_margin: 0.01 @@ -301,27 +262,6 @@ s770_active_business_income_tax: 3_240 net_investment_income_tax: 6_480 -- name: Custom NOL permission preserves the supplied deduction - period: 2026 - absolute_error_margin: 0.01 - reforms: policyengine_us.reforms.congress.s770.section_8.section_8 - input: - gov.contrib.congress.s770.section_8.nii_addon.in_effect: true - gov.contrib.congress.s770.section_8.active_business.in_effect: true - gov.contrib.congress.s770.section_8.active_business.deny_nol: false - net_investment_income: -10_000 - s770_niit_nol_deduction: 30_000 - s_corp_income: 20_000 - niit_magi: 250_000 - filing_status: SINGLE - output: - s770_nii_base: -10_000 - s770_nii_taxable_base: 0 - s770_active_taxable_base: 10_000 - s770_nii_addon_tax: 0 - s770_active_business_income_tax: 1_620 - net_investment_income_tax: 1_620 - - name: NII addon alone does not activate the active option NOL denial period: 2026 reforms: policyengine_us.reforms.congress.s770.section_8.section_8 @@ -355,184 +295,6 @@ net_investment_income_tax: 3_800 s770_revenue_oasdi: 0 -- name: The active option can start later than the NII addon - period: 2026 - absolute_error_margin: 0.01 - reforms: policyengine_us.reforms.congress.s770.section_8.section_8 - input: - gov.contrib.congress.s770.section_8.nii_addon.in_effect: true - gov.contrib.congress.s770.section_8.active_business.in_effect: true - gov.contrib.congress.s770.section_8.active_business.start_year: 2027 - taxable_interest_income: 100_000 - s_corp_income: 200_000 - niit_magi: 400_000 - filing_status: SINGLE - output: - s770_nii_addon_tax: 12_400 - s770_active_business_income_tax: 0 - net_investment_income_tax: 16_200 - -- name: The NII addon can start later than the active option - period: 2026 - absolute_error_margin: 0.01 - reforms: policyengine_us.reforms.congress.s770.section_8.section_8 - input: - gov.contrib.congress.s770.section_8.nii_addon.in_effect: true - gov.contrib.congress.s770.section_8.nii_addon.start_year: 2027 - gov.contrib.congress.s770.section_8.active_business.in_effect: true - taxable_interest_income: 100_000 - s_corp_income: 200_000 - niit_magi: 400_000 - filing_status: SINGLE - output: - s770_nii_addon_tax: 0 - s770_active_business_income_tax: 16_200 - net_investment_income_tax: 20_000 - -- name: Bill transfer mode sends 62 percent of all resulting NIIT to OASDI - period: 2026 - absolute_error_margin: 0.01 - reforms: policyengine_us.reforms.congress.s770.section_8.section_8 - input: - gov.contrib.congress.s770.section_8.nii_addon.in_effect: true - gov.contrib.congress.s770.section_8.active_business.in_effect: true - gov.contrib.congress.s770.section_8.transfer.mode: 3 - taxable_interest_income: 100_000 - s_corp_income: 200_000 - niit_magi: 400_000 - filing_status: SINGLE - output: - net_investment_income_tax: 32_400 - s770_existing_niit_revenue_oasdi: 2_356 - s770_nii_addon_revenue_oasdi: 7_688 - s770_active_business_income_revenue_oasdi: 10_044 - s770_revenue_oasdi: 20_088 - -- name: Combined rate allocation sends 12.4 of 16.2 percentage points to OASDI - period: 2026 - absolute_error_margin: 0.01 - reforms: policyengine_us.reforms.congress.s770.section_8.section_8 - input: - gov.contrib.congress.s770.section_8.nii_addon.in_effect: true - gov.contrib.congress.s770.section_8.active_business.in_effect: true - gov.contrib.congress.s770.section_8.transfer.mode: 1 - taxable_interest_income: 100_000 - s_corp_income: 200_000 - niit_magi: 400_000 - filing_status: SINGLE - output: - net_investment_income_tax: 32_400 - s770_existing_niit_revenue_oasdi: 3_800 * .124 / .162 - s770_nii_addon_revenue_oasdi: 12_400 * .124 / .162 - s770_active_business_income_revenue_oasdi: 12_400 - s770_revenue_oasdi: 24_800 - -- name: All addon mode transfers all new NII points and the chosen active share - period: 2026 - absolute_error_margin: 0.01 - reforms: policyengine_us.reforms.congress.s770.section_8.section_8 - input: - gov.contrib.congress.s770.section_8.nii_addon.in_effect: true - gov.contrib.congress.s770.section_8.active_business.in_effect: true - gov.contrib.congress.s770.section_8.transfer.mode: 2 - taxable_interest_income: 100_000 - s_corp_income: 200_000 - niit_magi: 400_000 - filing_status: SINGLE - output: - s770_existing_niit_revenue_oasdi: 0 - s770_nii_addon_revenue_oasdi: 12_400 - s770_active_business_income_revenue_oasdi: 10_044 - s770_revenue_oasdi: 22_444 - -- name: Combined rate transfer works with the active option independently enabled - period: 2026 - absolute_error_margin: 0.01 - reforms: policyengine_us.reforms.congress.s770.section_8.section_8 - input: - gov.contrib.congress.s770.section_8.nii_addon.in_effect: false - gov.contrib.congress.s770.section_8.active_business.in_effect: true - gov.contrib.congress.s770.section_8.transfer.mode: 1 - taxable_interest_income: 100_000 - s_corp_income: 200_000 - niit_magi: 400_000 - filing_status: SINGLE - output: - s770_nii_taxable_base: 100_000 - s770_active_taxable_base: 100_000 - s770_nii_addon_tax: 0 - s770_active_business_income_tax: 16_200 - net_investment_income_tax: 20_000 - # No NII add-on means no transfer of the existing 3.8 percent tax. - s770_existing_niit_revenue_oasdi: 0 - s770_nii_addon_revenue_oasdi: 0 - s770_active_business_income_revenue_oasdi: 12_400 - s770_revenue_oasdi: 12_400 - -- name: Combined rate transfer with zero addon preserves NII threshold consumption - period: 2026 - absolute_error_margin: 0.01 - reforms: policyengine_us.reforms.congress.s770.section_8.section_8 - input: - gov.contrib.congress.s770.section_8.nii_addon.in_effect: true - gov.contrib.congress.s770.section_8.nii_addon.rate: 0 - gov.contrib.congress.s770.section_8.active_business.in_effect: true - gov.contrib.congress.s770.section_8.transfer.mode: 1 - taxable_interest_income: 20_000 - s_corp_income: 20_000 - niit_magi: 230_000 - filing_status: SINGLE - output: - # NII still takes 20,000 of the shared 30,000 MAGI excess first. - s770_nii_taxable_base: 20_000 - s770_active_taxable_base: 10_000 - s770_nii_addon_tax: 0 - s770_active_business_income_tax: 1_620 - net_investment_income_tax: 2_380 - s770_existing_niit_revenue_oasdi: 0 - s770_nii_addon_revenue_oasdi: 0 - s770_active_business_income_revenue_oasdi: 1_240 - s770_revenue_oasdi: 1_240 - -- name: Custom transfer shares remain independently configurable - period: 2026 - absolute_error_margin: 0.01 - reforms: policyengine_us.reforms.congress.s770.section_8.section_8 - input: - gov.contrib.congress.s770.section_8.nii_addon.in_effect: true - gov.contrib.congress.s770.section_8.nii_addon.oasdi_share: .5 - gov.contrib.congress.s770.section_8.active_business.in_effect: true - gov.contrib.congress.s770.section_8.active_business.oasdi_share: .25 - taxable_interest_income: 100_000 - s_corp_income: 200_000 - niit_magi: 400_000 - filing_status: SINGLE - output: - s770_existing_niit_revenue_oasdi: 0 - s770_nii_addon_revenue_oasdi: 6_200 - s770_active_business_income_revenue_oasdi: 4_050 - s770_revenue_oasdi: 10_250 - -- name: Bill transfer has its own start year - period: 2026 - absolute_error_margin: 0.01 - reforms: policyengine_us.reforms.congress.s770.section_8.section_8 - input: - gov.contrib.congress.s770.section_8.nii_addon.in_effect: true - gov.contrib.congress.s770.section_8.active_business.in_effect: true - gov.contrib.congress.s770.section_8.transfer.mode: 3 - gov.contrib.congress.s770.section_8.transfer.start_year: 2027 - taxable_interest_income: 100_000 - s_corp_income: 200_000 - niit_magi: 400_000 - filing_status: SINGLE - output: - net_investment_income_tax: 32_400 - s770_existing_niit_revenue_oasdi: 0 - s770_nii_addon_revenue_oasdi: 0 - s770_active_business_income_revenue_oasdi: 0 - s770_revenue_oasdi: 0 - - name: Head and spouse aggregate but dependent income stays outside both bases period: 2026 absolute_error_margin: 0.01 diff --git a/policyengine_us/tests/policy/contrib/congress/s770/s770_active_business_income_revenue_oasdi.yaml b/policyengine_us/tests/policy/contrib/congress/s770/s770_active_business_income_revenue_oasdi.yaml new file mode 100644 index 00000000000..6a9879a8816 --- /dev/null +++ b/policyengine_us/tests/policy/contrib/congress/s770/s770_active_business_income_revenue_oasdi.yaml @@ -0,0 +1,69 @@ +- name: Combined rate transfer works with the active option independently enabled + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: false + gov.contrib.congress.s770.section_8.active_business.in_effect: true + gov.contrib.congress.s770.section_8.transfer.mode: 1 + taxable_interest_income: 100_000 + s_corp_income: 200_000 + niit_magi: 400_000 + filing_status: SINGLE + output: + s770_nii_taxable_base: 100_000 + s770_active_taxable_base: 100_000 + s770_nii_addon_tax: 0 + s770_active_business_income_tax: 16_200 + net_investment_income_tax: 20_000 + # No NII add-on means no transfer of the existing 3.8 percent tax. + s770_existing_niit_revenue_oasdi: 0 + s770_nii_addon_revenue_oasdi: 0 + s770_active_business_income_revenue_oasdi: 12_400 + s770_revenue_oasdi: 12_400 + +- name: Combined rate transfer with zero addon preserves NII threshold consumption + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.nii_addon.rate: 0 + gov.contrib.congress.s770.section_8.active_business.in_effect: true + gov.contrib.congress.s770.section_8.transfer.mode: 1 + taxable_interest_income: 20_000 + s_corp_income: 20_000 + niit_magi: 230_000 + filing_status: SINGLE + output: + # NII still takes 20,000 of the shared 30,000 MAGI excess first. + s770_nii_taxable_base: 20_000 + s770_active_taxable_base: 10_000 + s770_nii_addon_tax: 0 + s770_active_business_income_tax: 1_620 + net_investment_income_tax: 2_380 + s770_existing_niit_revenue_oasdi: 0 + s770_nii_addon_revenue_oasdi: 0 + s770_active_business_income_revenue_oasdi: 1_240 + s770_revenue_oasdi: 1_240 + +- name: Bill transfer has its own start year + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.active_business.in_effect: true + gov.contrib.congress.s770.section_8.transfer.mode: 3 + gov.contrib.congress.s770.section_8.transfer.start_year: 2027 + taxable_interest_income: 100_000 + s_corp_income: 200_000 + niit_magi: 400_000 + filing_status: SINGLE + output: + net_investment_income_tax: 32_400 + s770_existing_niit_revenue_oasdi: 0 + s770_nii_addon_revenue_oasdi: 0 + s770_active_business_income_revenue_oasdi: 0 + s770_revenue_oasdi: 0 + diff --git a/policyengine_us/tests/policy/contrib/congress/s770/s770_nii_taxable_base.yaml b/policyengine_us/tests/policy/contrib/congress/s770/s770_nii_taxable_base.yaml new file mode 100644 index 00000000000..de9b746a906 --- /dev/null +++ b/policyengine_us/tests/policy/contrib/congress/s770/s770_nii_taxable_base.yaml @@ -0,0 +1,94 @@ +- name: Zero addon rate still reserves NII threshold room + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.nii_addon.rate: 0 + gov.contrib.congress.s770.section_8.active_business.in_effect: true + taxable_interest_income: 20_000 + s_corp_income: 20_000 + niit_magi: 230_000 + filing_status: SINGLE + output: + s770_nii_taxable_base: 20_000 + s770_active_taxable_base: 10_000 + s770_nii_addon_tax: 0 + s770_active_business_income_tax: 1_620 + # .038 x 20,000 + .162 x 10,000. + net_investment_income_tax: 2_380 + +- name: Zero active rate does not change the NII allocation + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.active_business.in_effect: true + gov.contrib.congress.s770.section_8.active_business.rate: 0 + taxable_interest_income: 20_000 + s_corp_income: 20_000 + niit_magi: 230_000 + filing_status: SINGLE + output: + s770_nii_taxable_base: 20_000 + s770_active_taxable_base: 10_000 + s770_nii_addon_tax: 2_480 + s770_active_business_income_tax: 0 + net_investment_income_tax: 3_240 + +- name: Custom NOL permission preserves the supplied deduction + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.active_business.in_effect: true + gov.contrib.congress.s770.section_8.active_business.deny_nol: false + net_investment_income: -10_000 + s770_niit_nol_deduction: 30_000 + s_corp_income: 20_000 + niit_magi: 250_000 + filing_status: SINGLE + output: + s770_nii_base: -10_000 + s770_nii_taxable_base: 0 + s770_active_taxable_base: 10_000 + s770_nii_addon_tax: 0 + s770_active_business_income_tax: 1_620 + net_investment_income_tax: 1_620 + +- name: The active option can start later than the NII addon + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.active_business.in_effect: true + gov.contrib.congress.s770.section_8.active_business.start_year: 2027 + taxable_interest_income: 100_000 + s_corp_income: 200_000 + niit_magi: 400_000 + filing_status: SINGLE + output: + s770_nii_addon_tax: 12_400 + s770_active_business_income_tax: 0 + net_investment_income_tax: 16_200 + +- name: The NII addon can start later than the active option + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.nii_addon.start_year: 2027 + gov.contrib.congress.s770.section_8.active_business.in_effect: true + taxable_interest_income: 100_000 + s_corp_income: 200_000 + niit_magi: 400_000 + filing_status: SINGLE + output: + s770_nii_addon_tax: 0 + s770_active_business_income_tax: 16_200 + net_investment_income_tax: 20_000 + diff --git a/policyengine_us/tests/policy/contrib/congress/s770/s770_revenue_oasdi.yaml b/policyengine_us/tests/policy/contrib/congress/s770/s770_revenue_oasdi.yaml new file mode 100644 index 00000000000..5273c856572 --- /dev/null +++ b/policyengine_us/tests/policy/contrib/congress/s770/s770_revenue_oasdi.yaml @@ -0,0 +1,75 @@ +- name: Bill transfer mode sends 62 percent of all resulting NIIT to OASDI + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.active_business.in_effect: true + gov.contrib.congress.s770.section_8.transfer.mode: 3 + taxable_interest_income: 100_000 + s_corp_income: 200_000 + niit_magi: 400_000 + filing_status: SINGLE + output: + net_investment_income_tax: 32_400 + s770_existing_niit_revenue_oasdi: 2_356 + s770_nii_addon_revenue_oasdi: 7_688 + s770_active_business_income_revenue_oasdi: 10_044 + s770_revenue_oasdi: 20_088 + +- name: Combined rate allocation sends 12.4 of 16.2 percentage points to OASDI + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.active_business.in_effect: true + gov.contrib.congress.s770.section_8.transfer.mode: 1 + taxable_interest_income: 100_000 + s_corp_income: 200_000 + niit_magi: 400_000 + filing_status: SINGLE + output: + net_investment_income_tax: 32_400 + s770_existing_niit_revenue_oasdi: 3_800 * .124 / .162 + s770_nii_addon_revenue_oasdi: 12_400 * .124 / .162 + s770_active_business_income_revenue_oasdi: 12_400 + s770_revenue_oasdi: 24_800 + +- name: All addon mode transfers all new NII points and the chosen active share + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.active_business.in_effect: true + gov.contrib.congress.s770.section_8.transfer.mode: 2 + taxable_interest_income: 100_000 + s_corp_income: 200_000 + niit_magi: 400_000 + filing_status: SINGLE + output: + s770_existing_niit_revenue_oasdi: 0 + s770_nii_addon_revenue_oasdi: 12_400 + s770_active_business_income_revenue_oasdi: 10_044 + s770_revenue_oasdi: 22_444 + +- name: Custom transfer shares remain independently configurable + period: 2026 + absolute_error_margin: 0.01 + reforms: policyengine_us.reforms.congress.s770.section_8.section_8 + input: + gov.contrib.congress.s770.section_8.nii_addon.in_effect: true + gov.contrib.congress.s770.section_8.nii_addon.oasdi_share: .5 + gov.contrib.congress.s770.section_8.active_business.in_effect: true + gov.contrib.congress.s770.section_8.active_business.oasdi_share: .25 + taxable_interest_income: 100_000 + s_corp_income: 200_000 + niit_magi: 400_000 + filing_status: SINGLE + output: + s770_existing_niit_revenue_oasdi: 0 + s770_nii_addon_revenue_oasdi: 6_200 + s770_active_business_income_revenue_oasdi: 4_050 + s770_revenue_oasdi: 10_250 + From fde011f75d5e8cd300834ef142cd732b81186402 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 04:07:24 -0400 Subject: [PATCH 5/9] Document bounded test layout and normalize YAML endings --- policyengine_us/reforms/congress/s770/README.md | 9 +++++++++ .../s770/s770_active_business_income_revenue_oasdi.yaml | 1 - .../contrib/congress/s770/s770_nii_taxable_base.yaml | 1 - .../policy/contrib/congress/s770/s770_revenue_oasdi.yaml | 1 - 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/policyengine_us/reforms/congress/s770/README.md b/policyengine_us/reforms/congress/s770/README.md index f99545d3063..5e2a7cbab11 100644 --- a/policyengine_us/reforms/congress/s770/README.md +++ b/policyengine_us/reforms/congress/s770/README.md @@ -155,3 +155,12 @@ understates NII because passive pass-through income is missing. Microcosm #1251 repairs total/passive fields; #1261 splits the entities. Scoring remains provisional until the selected artifact actually contains both repairs and its stored fields are checked. No population effects are asserted here. + +## Test structure + +The YAML examples are grouped by reform configuration across four variable-named +files. Each file stays within the existing five-combination memory budget; +the Congress batcher can therefore release cached reform models between files. +Keep that grouping when adding cases rather than combining the files. Python +properties exercise the vector helpers, with one shared household simulation +checking dated registration, source aggregation and revenue-variable integration. diff --git a/policyengine_us/tests/policy/contrib/congress/s770/s770_active_business_income_revenue_oasdi.yaml b/policyengine_us/tests/policy/contrib/congress/s770/s770_active_business_income_revenue_oasdi.yaml index 6a9879a8816..a7b58828857 100644 --- a/policyengine_us/tests/policy/contrib/congress/s770/s770_active_business_income_revenue_oasdi.yaml +++ b/policyengine_us/tests/policy/contrib/congress/s770/s770_active_business_income_revenue_oasdi.yaml @@ -66,4 +66,3 @@ s770_nii_addon_revenue_oasdi: 0 s770_active_business_income_revenue_oasdi: 0 s770_revenue_oasdi: 0 - diff --git a/policyengine_us/tests/policy/contrib/congress/s770/s770_nii_taxable_base.yaml b/policyengine_us/tests/policy/contrib/congress/s770/s770_nii_taxable_base.yaml index de9b746a906..0f67af85071 100644 --- a/policyengine_us/tests/policy/contrib/congress/s770/s770_nii_taxable_base.yaml +++ b/policyengine_us/tests/policy/contrib/congress/s770/s770_nii_taxable_base.yaml @@ -91,4 +91,3 @@ s770_nii_addon_tax: 0 s770_active_business_income_tax: 16_200 net_investment_income_tax: 20_000 - diff --git a/policyengine_us/tests/policy/contrib/congress/s770/s770_revenue_oasdi.yaml b/policyengine_us/tests/policy/contrib/congress/s770/s770_revenue_oasdi.yaml index 5273c856572..62961759a59 100644 --- a/policyengine_us/tests/policy/contrib/congress/s770/s770_revenue_oasdi.yaml +++ b/policyengine_us/tests/policy/contrib/congress/s770/s770_revenue_oasdi.yaml @@ -72,4 +72,3 @@ s770_nii_addon_revenue_oasdi: 6_200 s770_active_business_income_revenue_oasdi: 4_050 s770_revenue_oasdi: 10_250 - From 8038424a4ff4b917503f8b5bd51acc3694a147dc Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 04:12:18 -0400 Subject: [PATCH 6/9] Clarify activation conditions for unified liability --- policyengine_us/reforms/congress/s770/README.md | 5 +++-- policyengine_us/tests/reforms/test_s770_section_8.py | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/policyengine_us/reforms/congress/s770/README.md b/policyengine_us/reforms/congress/s770/README.md index 5e2a7cbab11..78d0a7fd928 100644 --- a/policyengine_us/reforms/congress/s770/README.md +++ b/policyengine_us/reforms/congress/s770/README.md @@ -44,8 +44,9 @@ the existing 3.8 percent NIIT also uses `n`. The reform replaces component taxes are already included there and in federal income tax; adding them again would double count. -With identical sources, exclusions, NOL treatment, MAGI, thresholds and timing, -and `existing_rate + addon_rate = active_rate = .162`, this equals `.162 * L` +With both options enabled, identical sources, exclusions, NOL treatment, MAGI, +thresholds and timing, and `existing_rate + addon_rate = active_rate = .162`, +this equals `.162 * L` for all signed inputs. This is a conditional liability identity, not a claim that the available fields implement every statutory category. For `N=−10,000`, `A=20,000`, `X=50,000`, the tax is $1,620. For `N=20,000`, `A=−10,000`, the diff --git a/policyengine_us/tests/reforms/test_s770_section_8.py b/policyengine_us/tests/reforms/test_s770_section_8.py index cb4e0a5880b..e84b8c7dde1 100644 --- a/policyengine_us/tests/reforms/test_s770_section_8.py +++ b/policyengine_us/tests/reforms/test_s770_section_8.py @@ -7,9 +7,9 @@ Direct-NII YAML cases cover the NOL-denial hook separately from these source aggregation checks. -Unified-liability equivalence assumes identical signed sources, exclusions, -NOL treatment, MAGI, thresholds and timing, with existing rate + add-on rate = -active rate = 16.2%. It does not imply equal OASDI transfers or full statutory +With both options enabled, unified-liability equivalence assumes identical signed +sources, exclusions, NOL treatment, MAGI, thresholds and timing, with existing +rate + add-on rate = active rate = 16.2%. It does not imply equal OASDI transfers or full statutory category coverage. Adding a signed active loss can lower total tax, so the monotonicity property requires a nonnegative active residual and fixed signed NII/source rules, including exclusions and NOL treatment, across switches. From 24192e504c0c1d533a7f6c37f4be931ef9587455 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 23 Oct 2026 04:59:37 -0400 Subject: [PATCH 7/9] Clarify trust fund effective dates after enactment --- policyengine_us/reforms/congress/s770/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/policyengine_us/reforms/congress/s770/README.md b/policyengine_us/reforms/congress/s770/README.md index 78d0a7fd928..9cf73485fb1 100644 --- a/policyengine_us/reforms/congress/s770/README.md +++ b/policyengine_us/reforms/congress/s770/README.md @@ -18,8 +18,9 @@ of this Act”. There is no statutory fixed start year. The configurable 2026 defaults illustrate assumed enactment timing and have no effect while disabled. Section 9(a) assigns 62 percent of all section 1411 tax to a merged Social -Security fund. It appropriates the revenue beginning with the first fiscal year -after enactment; the merged fund receives assets on the following January 1. +Security fund. Revenue appropriation starts with the first fiscal year beginning +after enactment; assets transfer on January 1 of the first calendar year beginning +after enactment. `transfer.start_year` is a separate annual modeling choice. This liability model does not estimate fiscal-year collection lags or prorate receipts. From 0e66a950da8dd972ea0b39784eb015e558bf4f13 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 27 Oct 2026 05:36:48 -0700 Subject: [PATCH 8/9] Cover the owners-draw netting factor guard Zero the SE netting factor q = 1 - employer share x (OASDI + HI SE rates) for 2026 only, so s770_active_business_income raises its positivity error there, while 2025 keeps q = 0.9235 and excludes 9,235 / 0.9235 = 10,000 of a 100,000 S-corp profit. Co-Authored-By: Claude Opus 5.5 --- .../tests/reforms/test_s770_section_8.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/policyengine_us/tests/reforms/test_s770_section_8.py b/policyengine_us/tests/reforms/test_s770_section_8.py index e84b8c7dde1..30cae9c1220 100644 --- a/policyengine_us/tests/reforms/test_s770_section_8.py +++ b/policyengine_us/tests/reforms/test_s770_section_8.py @@ -389,3 +389,40 @@ def test_dated_registration_components_and_revenue_accounts(dated_simulation, ye np.testing.assert_allclose( sim.calculate("s770_revenue_oasdi", year), sum(revenues), atol=0.01 ) + + +class NoDrawNettingFactorIn2026(Reform): + """In 2026 only, q = 1 - 100% x (100% + 0%) = 0 leaves no SE netting factor.""" + + def apply(self): + def modify(parameters): + irs = parameters.gov.irs + for node, value in ( + (irs.ald.misc.employer_share, 1), + (irs.self_employment.rate.social_security, 1), + (irs.self_employment.rate.medicare, 0), + ): + node.update(period="2026", value=value) + return parameters + + self.modify_parameters(modify) + + +def test_owners_draw_netting_factor_must_be_positive(): + # An OASDI-only draw is excluded as its taxed base divided by q. + head = { + variable: {"2025": value, "2026": value} + for variable, value in ( + ("age", 45), + ("s_corp_income", 100_000), + ("s770_draw_oasdi_base", 9_235), + ) + } + situation = {"people": {"head": head}} + sim = Simulation(situation=situation, reform=NoDrawNettingFactorIn2026) + # Current law: q = 1 - 50% x (12.4% + 2.9%) = 0.9235; 9,235 / q = 10,000. + np.testing.assert_allclose( + sim.calculate("s770_active_business_income", 2025), [90_000], atol=0.01 + ) + with pytest.raises(ValueError, match="netting factor must be positive"): + sim.calculate("s770_active_business_income", 2026) From 8c3b927a906061f32aac02a2ab15f5fc2708326d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 27 Oct 2026 15:07:59 -0700 Subject: [PATCH 9/9] Limit calculator differential reference removal to S770 section 2 --- policyengine_us/tests/unit/test_ss_calculator_differential.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/policyengine_us/tests/unit/test_ss_calculator_differential.py b/policyengine_us/tests/unit/test_ss_calculator_differential.py index 4b4ad08e875..3228ede8e5b 100644 --- a/policyengine_us/tests/unit/test_ss_calculator_differential.py +++ b/policyengine_us/tests/unit/test_ss_calculator_differential.py @@ -26,7 +26,7 @@ # Benefit levers and COLA history also serve the reported-benefit route, # which remains part of the reference when the calculator is removed. "gov.contrib.ssa.pia", - "gov.contrib.congress.s770", + "gov.contrib.congress.s770.section_2", "gov.ssa.social_security.pia.bend_points", "gov.ssa.social_security.cola", )