Files
grant-outreach-engine/apps/outreach-review/app/routes/matches.$matchId.tsx
Croissant Le Doux 88affbeb4f feat(scoring): federal funder precedent via USASpending (ALN-level)
Precedent was always a valid criterion for federal grants — the index
just didn't cover them. Now: ingest captures ALN/CFDA numbers from
fetchOpportunity (grants.alns); nightly federalPrecedent workflow
(04:45) queries USASpending award search per distinct program (free
official API, 3-year NH lookback) and stamps program_state_award_count
+ sample recipients onto open grants (multi-ALN keeps highest). Match
retrieval feeds the same funderStateGrantCount input and 25-point tiers
foundations use; detail page shows the recipients-evidence table with
an incumbent-renewal caution.

Also: detail refresh now rotates oldest-verified-first
(serverMapGrantVerification) — the Set-based partition re-fetched the
same 200 every pass, leaving 365/565 grants ALN-less.

Live: 564/564 grants ALN-tagged, 156 programs swept, 85 with NH
history, 468 grants carrying precedent, first federal easy-wins (66pts,
25/25 precedent, Aug-24 deadline). 158 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 23:18:02 -04:00

332 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { nteeDescription } from '@novelpad/outreach-core';
import {
serverGetMatchDetail,
serverSetMatchReview,
} from '@novelpad/outreach-core/server';
import { Form, Link, redirect } from 'react-router';
import { db } from '#~/db.server.js';
import type { Route } from './+types/matches.$matchId.js';
export async function loader({ request, params }: Route.LoaderArgs) {
if (request.method === 'HEAD') return;
const detail = await serverGetMatchDetail(db, params.matchId);
if (detail == null) {
throw new Response('Match not found', { status: 404 });
}
return { detail };
}
export async function action({ request, params }: Route.ActionArgs) {
if (request.method === 'HEAD') return;
const formData = await request.formData();
const decision = formData.get('decision');
if (decision !== 'approved' && decision !== 'rejected') {
throw new Response('Invalid review submission', { status: 400 });
}
await serverSetMatchReview(db, {
matchId: params.matchId,
reviewStatus: decision,
rejectReason: decision === 'rejected' ? 'other' : undefined,
});
return redirect('/');
}
function dollars(n: number | null): string {
if (n == null) return '—';
return `$${n.toLocaleString('en-US')}`;
}
const SUBSCORE_MAX: Record<string, number> = {
missionFit: 30,
funderPrecedent: 25,
capacityFit: 15,
competition: 15,
effort: 10,
runway: 5,
};
export default function MatchDetail({ loaderData }: Route.ComponentProps) {
const { detail } = loaderData ?? {};
if (detail == null) return null;
const { match, org, grant, funderGivingHistory, funderApplicationInfo } =
detail;
const applyInfo = (funderApplicationInfo ?? null) as {
preselectedOnly?: boolean;
recipientName?: string;
formAndInfoAndMaterials?: string;
submissionDeadlines?: string;
restrictionsOnAwards?: string;
} | null;
const subscores = (match.subscores ?? {}) as Record<string, number>;
const rationale = (match.rationale ?? {}) as Record<string, unknown>;
const nhHistory = funderGivingHistory;
return (
<main className="container mx-auto max-w-4xl space-y-6 p-6">
<nav>
<Link to="/" className="text-blue-700 underline">
Back to queue
</Link>
</nav>
<header className="flex items-start justify-between gap-4">
<div>
<h1 className="text-2xl font-semibold">
{org.name} {grant.funder}
</h1>
<p className="text-gray-600">
Score {match.totalScore}/100
{match.easyWin ? ' · Easy win' : ''}
{match.isHero ? ' · Hero' : ''} · Status: {match.reviewStatus}
</p>
</div>
<div className="flex shrink-0 gap-2">
<Form method="post">
<input type="hidden" name="decision" value="approved" />
<button
type="submit"
className="rounded bg-green-600 px-4 py-2 text-white hover:bg-green-700"
>
Approve
</button>
</Form>
<Form method="post">
<input type="hidden" name="decision" value="rejected" />
<button
type="submit"
className="rounded bg-red-600 px-4 py-2 text-white hover:bg-red-700"
>
Reject
</button>
</Form>
</div>
</header>
<section className="grid gap-6 md:grid-cols-2">
<div className="rounded border p-4">
<h2 className="mb-2 text-lg font-semibold">Organization</h2>
<dl className="space-y-1 text-sm">
<div>
<dt className="inline font-medium">Location: </dt>
<dd className="inline">
{[org.city, org.state].filter(Boolean).join(', ')}
</dd>
</div>
<div>
<dt className="inline font-medium">Focus (NTEE): </dt>
<dd className="inline">
{org.nteeCode ?? '—'}
{nteeDescription(org.nteeCode) != null &&
`${nteeDescription(org.nteeCode)}`}
</dd>
</div>
<div>
<dt className="inline font-medium">Annual revenue: </dt>
<dd className="inline">{dollars(org.totalRevenue)}</dd>
</div>
<div>
<dt className="inline font-medium">Mission (profile): </dt>
<dd className="inline">
{org.missionStatement ?? '—'}
{org.profileConfidence != null &&
org.profileConfidence <= 0.2 && (
<span className="text-amber-700">
{' '}
(auto-generated stub verify on their website)
</span>
)}
</dd>
</div>
</dl>
<div className="mt-3 space-x-3 text-sm">
{org.ein != null && (
<a
className="text-blue-700 underline"
href={`https://projects.propublica.org/nonprofits/organizations/${org.ein.replace(/^0+/, '')}`}
target="_blank"
rel="noreferrer"
>
ProPublica filings
</a>
)}
<a
className="text-blue-700 underline"
href={`https://www.google.com/search?q=${encodeURIComponent(`"${org.name}" ${org.city ?? ''} NH nonprofit`)}`}
target="_blank"
rel="noreferrer"
>
Search their site
</a>
</div>
</div>
<div className="rounded border p-4">
<h2 className="mb-2 text-lg font-semibold">Grant</h2>
<dl className="space-y-1 text-sm">
<div>
<dt className="inline font-medium">Title: </dt>
<dd className="inline">{grant.title}</dd>
</div>
<div>
<dt className="inline font-medium">Award: </dt>
<dd className="inline">
{dollars(grant.awardFloor)} {dollars(grant.awardCeiling)}
</dd>
</div>
<div>
<dt className="inline font-medium">Closes: </dt>
<dd className="inline">
{grant.closeDate == null
? 'Rolling / no stated deadline'
: new Date(grant.closeDate).toLocaleDateString('en-US')}
</dd>
</div>
<div>
<dt className="inline font-medium">Source: </dt>
<dd className="inline">{grant.source}</dd>
</div>
{grant.eligibilityEntityTypes != null &&
grant.eligibilityEntityTypes.length > 0 && (
<div>
<dt className="inline font-medium">Eligibility: </dt>
<dd className="inline">
{grant.eligibilityEntityTypes.join('; ')}
</dd>
</div>
)}
</dl>
<div className="mt-3 text-sm">
<a
className="text-blue-700 underline"
href={grant.sourceUrl}
target="_blank"
rel="noreferrer"
>
{grant.source === 'irs_990pf'
? 'Funder on ProPublica ↗'
: 'Grant listing ↗'}
</a>
</div>
</div>
</section>
<section className="rounded border p-4">
<h2 className="mb-2 text-lg font-semibold">Score breakdown</h2>
<table className="w-full max-w-md text-sm">
<tbody>
{Object.entries(SUBSCORE_MAX).map(([key, max]) => (
<tr key={key} className="border-b">
<td className="py-1 capitalize">
{key.replace(/([A-Z])/g, ' $1').toLowerCase()}
</td>
<td className="py-1 text-right tabular-nums">
{subscores[key] ?? 0} / {max}
</td>
</tr>
))}
</tbody>
</table>
{typeof rationale.similarity === 'number' && (
<p className="mt-2 text-sm text-gray-600">
Embedding similarity: {(rationale.similarity as number).toFixed(3)}
{Array.isArray(rationale.gateFailures) &&
(rationale.gateFailures as string[]).length > 0 &&
` · Ignored gate flags: ${(rationale.gateFailures as string[]).join(', ')}`}
</p>
)}
</section>
{applyInfo != null && (
<section className="rounded border p-4">
<h2 className="mb-2 text-lg font-semibold">
How to apply (funder's own 990-PF, Part XV)
</h2>
{applyInfo.preselectedOnly === true && (
<p className="mb-2 rounded bg-red-100 p-2 text-sm font-medium text-red-800">
This funder states it only contributes to preselected
organizations and does not accept unsolicited requests — do
not pitch.
</p>
)}
<dl className="space-y-1 text-sm">
{applyInfo.formAndInfoAndMaterials != null && (
<div>
<dt className="inline font-medium">Application form: </dt>
<dd className="inline">{applyInfo.formAndInfoAndMaterials}</dd>
</div>
)}
{applyInfo.submissionDeadlines != null && (
<div>
<dt className="inline font-medium">Deadlines: </dt>
<dd className="inline">{applyInfo.submissionDeadlines}</dd>
</div>
)}
{applyInfo.restrictionsOnAwards != null && (
<div>
<dt className="inline font-medium">Restrictions: </dt>
<dd className="inline">{applyInfo.restrictionsOnAwards}</dd>
</div>
)}
{applyInfo.recipientName != null && (
<div>
<dt className="inline font-medium">Applications to: </dt>
<dd className="inline">{applyInfo.recipientName}</dd>
</div>
)}
</dl>
</section>
)}
<section className="rounded border p-4">
<h2 className="mb-2 text-lg font-semibold">Grant synopsis</h2>
<p className="whitespace-pre-wrap text-sm">{grant.synopsis ?? ''}</p>
</section>
{nhHistory.length > 0 && (
<section className="rounded border p-4">
<h2 className="mb-1 text-lg font-semibold">
{grant.source === 'irs_990pf'
? 'Funder giving history (from 990-PF filings)'
: 'Recent program awards to NH recipients (USASpending)'}
</h2>
<p className="mb-3 text-sm text-gray-600">
The concrete evidence behind the precedent score —{' '}
{grant.source === 'irs_990pf'
? 'who this funder actually paid, sorted in-state first.'
: 'who this federal program actually funded in NH recently. Watch for one incumbent recapturing renewals vs. genuine spread across orgs.'}
</p>
<table className="w-full text-sm">
<thead>
<tr className="border-b text-left">
<th className="py-1 pr-2">Recipient</th>
<th className="py-1 pr-2">City</th>
<th className="py-1 pr-2 text-right">Amount</th>
<th className="py-1 pr-2">Year</th>
<th className="py-1">Purpose</th>
</tr>
</thead>
<tbody>
{nhHistory.map((g, i) => (
<tr key={i} className="border-b align-top">
<td className="py-1 pr-2">{g.recipientName}</td>
<td className="py-1 pr-2">{g.recipientCity ?? ''}</td>
<td className="py-1 pr-2 text-right tabular-nums">
{dollars(g.amount)}
</td>
<td className="py-1 pr-2">{g.taxYear}</td>
<td className="py-1">{g.purpose ?? ''}</td>
</tr>
))}
</tbody>
</table>
</section>
)}
</main>
);
}