feat(review+990pf): Part XV application info — parse the preselected-only kill signal, surface how-to-apply at review

Parser captures 990-PF Part XV line 2 (OnlyContriToPreselectedInd) even
when the application group is absent. Synthesis skips preselected-only
funders and closes their previously-synthesized grant rows
(serverCloseGrantsForFunders). Match detail page gains a 'How to apply
(funder's own 990-PF, Part XV)' panel — application form, deadlines,
restrictions, red do-not-pitch banner — fed by serverGetMatchDetail.

Backfill re-parse across all 747 NH foundations: 192 are preselected-
only; 62 of 123 synthesized foundation leads were unpitchable by their
funder's own filing and are now closed, with 24,436 stale pending
matches cleaned. Post re-match: 714 easy wins across 327 orgs, every
foundation lead now backed by a funder that actually accepts
applications. 156 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Croissant Le Doux
2026-07-16 21:46:58 -04:00
parent d8ca133e95
commit a92b75b315
7 changed files with 166 additions and 3 deletions

View File

@@ -40,6 +40,9 @@ export interface MatchDetail {
geographicScope: string | null;
applicationEffortEstimate: string;
};
/** Part XV application info from the funder's latest parsed 990-PF
* (form required, deadlines, restrictions, preselected-only flag). */
funderApplicationInfo: unknown;
/** For 990-PF matches: the funder's actual giving history into the
* org's state — the concrete evidence behind the precedent subscore. */
funderGivingHistory: Array<{
@@ -105,7 +108,14 @@ export async function serverGetMatchDetail(
.limit(1);
let funderGivingHistory: MatchDetail['funderGivingHistory'] = [];
let funderApplicationInfo: unknown = null;
if (row.grantFunderEin != null) {
const funderRow = await db
.select({ applicationInfo: schema.funders.applicationInfo })
.from(schema.funders)
.where(eq(schema.funders.ein, row.grantFunderEin))
.limit(1);
funderApplicationInfo = funderRow[0]?.applicationInfo ?? null;
const funderRows = await db
.select({
recipientName: schema.funderGrants.recipientName,
@@ -172,6 +182,7 @@ export async function serverGetMatchDetail(
geographicScope: row.grantGeo,
applicationEffortEstimate: row.grantEffort,
},
funderApplicationInfo,
funderGivingHistory,
};
}