diff --git a/frontend/src/Committee.svelte b/frontend/src/Committee.svelte index 273eb72..b4dae19 100644 --- a/frontend/src/Committee.svelte +++ b/frontend/src/Committee.svelte @@ -327,7 +327,6 @@ SELECT * FROM final`;
diff --git a/frontend/src/forms/F3/F3.svelte b/frontend/src/forms/F3/F3.svelte index 0f0d0d9..c594cf9 100644 --- a/frontend/src/forms/F3/F3.svelte +++ b/frontend/src/forms/F3/F3.svelte @@ -176,7 +176,6 @@
diff --git a/frontend/src/forms/shared/StateContributors.svelte b/frontend/src/forms/shared/StateContributors.svelte index ea140f1..8140dce 100644 --- a/frontend/src/forms/shared/StateContributors.svelte +++ b/frontend/src/forms/shared/StateContributors.svelte @@ -15,10 +15,9 @@ interface Props { scope: FilingScope; homeState?: string | null; - formTypeFilter?: string; } - let { scope, homeState = null, formTypeFilter }: Props = $props(); + let { scope, homeState = null }: Props = $props(); const dbName = get(databaseName); @@ -60,16 +59,29 @@ } function stateUrl(stateCode: string): string { - return buildStateUrl(dbName, scope, stateCode, formTypeFilter, filingIdsResult.data); + return buildStateUrl(dbName, scope, stateCode, filingIdsResult.data); } const total = $derived( (contributions.data ?? []).reduce((sum, row) => sum + (row.total_contributions || 0), 0) ); + // On F3X (PACs, party committees, super PACs) line 11(a)(i) also carries + // contributions from corporations, LLCs and other non-committee "persons", + // which can dwarf the money from actual individuals at super PACs. + const isF3X = $derived(metadata.data?.form_type === 'F3X'); + const title = $derived( + isF3X + ? 'Contributions from Individuals & Other Persons by State' + : 'Individual Contributions by State' + ); + const subtitle = $derived(() => { const m = metadata.data; if (!m?.committee_name) return null; - const parts = [`Top individual contributions to ${m.committee_name}`]; + const kind = isF3X + ? 'contributions from individuals and other persons' + : 'individual contributions'; + const parts = [`Top ${kind} to ${m.committee_name}`]; if (m.coverage_from_date && m.coverage_through_date) { parts[0] += ` between ${formatDate(m.coverage_from_date)} and ${formatDate(m.coverage_through_date)}`; } @@ -79,10 +91,7 @@ const sourceNote = $derived(() => { const formType = metadata.data?.form_type; if (!formType) return null; - if (formTypeFilter === 'SA11AI') { - return `Source: ${formType}, Schedule A, Line 11(a)(i)`; - } - return `Source: ${formType}, Schedule A`; + return `Source: ${formType}, Schedule A, Line 11(a)(i) and Line 12 joint fundraising memo entries`; }); const N = 10; @@ -96,12 +105,12 @@ {#if contributions.isLoading}
-

Individual Contributions by State

+

{title}

Loading...
{:else if contributions.data && contributions.data.length > 0}
-

Individual Contributions by State

+

{title}

{#if subtitle()}

{subtitle()}

{/if} @@ -146,7 +155,15 @@
-

Only includes individuals who have given $200 or more this cycle.

+

+ Only includes itemized contributions (generally donors who have given more than $200 this + cycle). +

+ {#if isF3X} +

+ Includes contributions from corporations, LLCs and other non-committee sources. +

+ {/if} {#if sourceNote()}

{sourceNote()}

{/if} diff --git a/frontend/src/forms/shared/stateContributorsFetcher.ts b/frontend/src/forms/shared/stateContributorsFetcher.ts index b5604ba..b5c404e 100644 --- a/frontend/src/forms/shared/stateContributorsFetcher.ts +++ b/frontend/src/forms/shared/stateContributorsFetcher.ts @@ -40,6 +40,25 @@ export async function fetchScopeMetadata( ); } +/** + * Schedule A rows that represent contributions from individuals, matching how + * FEC.gov builds its by-state breakdown: + * - Line 11(a)(i) itemized individual contributions (non-memo). Earmarked + * contributions (e.g. via WinRed) are itemized here under the donor; the + * conduit's own "earmarked-conduit details" rows are memo entries. + * - Line 12 memo entries for individuals (and tribes, which FEC also counts): + * the donors behind joint fundraising committee transfers. The non-memo line + * 12 row is the transfer from the JFC itself, which would otherwise be + * attributed to the JFC's state. + * + * Partnership/LLC contributions are counted once, via the non-memo 11(a)(i) + * row; the memo attributions to individual partners are skipped. + */ +export const INDIVIDUAL_CONTRIBUTIONS_WHERE = `( + (form_type = 'SA11AI' AND memo_code IS NOT 'X') + OR (form_type = 'SA12' AND memo_code = 'X' AND entity_type IN ('IND', 'ORG')) +)`; + export function fetchStateContributions( dbName: string, scope: FilingScope @@ -54,7 +73,7 @@ export function fetchStateContributions( WHERE ${where} AND contributor_state IS NOT NULL AND contributor_state != '' - AND memo_code != 'X' + AND ${INDIVIDUAL_CONTRIBUTIONS_WHERE} GROUP BY contributor_state ORDER BY total_contributions DESC `; @@ -65,7 +84,6 @@ export function buildStateUrl( dbName: string, scope: FilingScope, stateCode: string, - formTypeFilter?: string, filingIds?: string[] ): string { const scopeParams = filingScopeUrlParams(scope, filingIds); @@ -73,10 +91,7 @@ export function buildStateUrl( _sort: 'rowid', contributor_state__exact: stateCode, ...scopeParams, - memo_code__not: 'X', + _where: INDIVIDUAL_CONTRIBUTIONS_WHERE.replace(/\s+/g, ' ').trim(), }); - if (formTypeFilter) { - params.set('form_type__exact', formTypeFilter); - } return `/${dbName}/libfec_schedule_a?${params}`; }