feat(review): match detail view — evidence for fit at review time
/matches/:matchId shows the org (location, NTEE + description, revenue, profile mission w/ stub warning, ProPublica + web-search links), the grant (award band, deadline, eligibility, source link), the full subscore breakdown with embedding similarity + ignored-gate flags, the grant synopsis, and — for 990-PF matches — the funder's actual giving history table (recipient, city, amount, year, purpose; in-state first), which is the concrete evidence behind the precedent score. Approve/ reject on the detail page redirects back to the queue; queue org names link through. Core: serverGetMatchDetail (match+org+grant join, profile, up to 40 funder-grant rows in-state-first). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
import { desc, eq } from 'drizzle-orm';
|
||||
|
||||
import type { NpOutreachDatabase, NpOutreachTransaction } from '#~/db/db.js';
|
||||
import { schema } from '#~/db/db.js';
|
||||
|
||||
export interface MatchDetail {
|
||||
match: {
|
||||
id: string;
|
||||
totalScore: number;
|
||||
subscores: unknown;
|
||||
rationale: unknown;
|
||||
easyWin: boolean;
|
||||
isHero: boolean;
|
||||
reviewStatus: 'pending' | 'approved' | 'rejected' | 'edited';
|
||||
};
|
||||
org: {
|
||||
id: string;
|
||||
name: string;
|
||||
city: string | null;
|
||||
state: string;
|
||||
ein: string | null;
|
||||
nteeCode: string | null;
|
||||
totalRevenue: number | null;
|
||||
registrationNumber: string | null;
|
||||
missionStatement: string | null;
|
||||
profileConfidence: number | null;
|
||||
};
|
||||
grant: {
|
||||
id: string;
|
||||
title: string;
|
||||
funder: string;
|
||||
funderEin: string | null;
|
||||
synopsis: string | null;
|
||||
awardFloor: number | null;
|
||||
awardCeiling: number | null;
|
||||
closeDate: Date | null;
|
||||
sourceUrl: string;
|
||||
source: string;
|
||||
eligibilityEntityTypes: string[] | null;
|
||||
geographicScope: string | null;
|
||||
applicationEffortEstimate: string;
|
||||
};
|
||||
/** For 990-PF matches: the funder's actual giving history into the
|
||||
* org's state — the concrete evidence behind the precedent subscore. */
|
||||
funderGivingHistory: Array<{
|
||||
recipientName: string;
|
||||
recipientCity: string | null;
|
||||
amount: number | null;
|
||||
purpose: string | null;
|
||||
taxYear: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export async function serverGetMatchDetail(
|
||||
db: NpOutreachDatabase | NpOutreachTransaction,
|
||||
matchId: string,
|
||||
): Promise<MatchDetail | null> {
|
||||
const rows = await db
|
||||
.select({
|
||||
matchId: schema.matches.id,
|
||||
totalScore: schema.matches.totalScore,
|
||||
subscores: schema.matches.subscores,
|
||||
rationale: schema.matches.rationale,
|
||||
easyWin: schema.matches.easyWin,
|
||||
isHero: schema.matches.isHero,
|
||||
reviewStatus: schema.matches.reviewStatus,
|
||||
orgId: schema.orgs.id,
|
||||
orgName: schema.orgs.name,
|
||||
orgCity: schema.orgs.city,
|
||||
orgState: schema.orgs.state,
|
||||
orgEin: schema.orgs.ein,
|
||||
orgNtee: schema.orgs.nteeCode,
|
||||
orgRevenue: schema.orgs.totalRevenue,
|
||||
orgRegNo: schema.orgs.registrationNumber,
|
||||
grantId: schema.grants.id,
|
||||
grantTitle: schema.grants.title,
|
||||
grantFunder: schema.grants.funder,
|
||||
grantFunderEin: schema.grants.funderEin,
|
||||
grantSynopsis: schema.grants.synopsis,
|
||||
grantFloor: schema.grants.awardFloor,
|
||||
grantCeiling: schema.grants.awardCeiling,
|
||||
grantClose: schema.grants.closeDate,
|
||||
grantSourceUrl: schema.grants.sourceUrl,
|
||||
grantSource: schema.grants.source,
|
||||
grantEligibility: schema.grants.eligibilityEntityTypes,
|
||||
grantGeo: schema.grants.geographicScope,
|
||||
grantEffort: schema.grants.applicationEffortEstimate,
|
||||
})
|
||||
.from(schema.matches)
|
||||
.innerJoin(schema.orgs, eq(schema.matches.orgId, schema.orgs.id))
|
||||
.innerJoin(schema.grants, eq(schema.matches.grantId, schema.grants.id))
|
||||
.where(eq(schema.matches.id, matchId))
|
||||
.limit(1);
|
||||
|
||||
const row = rows[0];
|
||||
if (row == null) return null;
|
||||
|
||||
const profiles = await db
|
||||
.select({
|
||||
missionStatement: schema.orgProfiles.missionStatement,
|
||||
confidence: schema.orgProfiles.confidence,
|
||||
})
|
||||
.from(schema.orgProfiles)
|
||||
.where(eq(schema.orgProfiles.orgId, row.orgId))
|
||||
.limit(1);
|
||||
|
||||
let funderGivingHistory: MatchDetail['funderGivingHistory'] = [];
|
||||
if (row.grantFunderEin != null) {
|
||||
const funderRows = await db
|
||||
.select({
|
||||
recipientName: schema.funderGrants.recipientName,
|
||||
recipientCity: schema.funderGrants.recipientCity,
|
||||
recipientState: schema.funderGrants.recipientState,
|
||||
amount: schema.funderGrants.amount,
|
||||
purpose: schema.funderGrants.purpose,
|
||||
taxYear: schema.funderGrants.taxYear,
|
||||
})
|
||||
.from(schema.funderGrants)
|
||||
.innerJoin(
|
||||
schema.funders,
|
||||
eq(schema.funderGrants.funderId, schema.funders.id),
|
||||
)
|
||||
.where(eq(schema.funders.ein, row.grantFunderEin))
|
||||
.orderBy(desc(schema.funderGrants.taxYear), desc(schema.funderGrants.amount))
|
||||
.limit(200);
|
||||
|
||||
// In-state grants first (the precedent evidence), then the rest.
|
||||
funderGivingHistory = [...funderRows]
|
||||
.sort((a, b) => {
|
||||
const aIn = a.recipientState === row.orgState ? 0 : 1;
|
||||
const bIn = b.recipientState === row.orgState ? 0 : 1;
|
||||
return aIn - bIn;
|
||||
})
|
||||
.slice(0, 40)
|
||||
.map(({ recipientState: _s, ...rest }) => rest);
|
||||
}
|
||||
|
||||
return {
|
||||
match: {
|
||||
id: row.matchId,
|
||||
totalScore: row.totalScore,
|
||||
subscores: row.subscores,
|
||||
rationale: row.rationale,
|
||||
easyWin: row.easyWin,
|
||||
isHero: row.isHero,
|
||||
reviewStatus: row.reviewStatus,
|
||||
},
|
||||
org: {
|
||||
id: row.orgId,
|
||||
name: row.orgName,
|
||||
city: row.orgCity,
|
||||
state: row.orgState,
|
||||
ein: row.orgEin,
|
||||
nteeCode: row.orgNtee,
|
||||
totalRevenue: row.orgRevenue,
|
||||
registrationNumber: row.orgRegNo,
|
||||
missionStatement: profiles[0]?.missionStatement ?? null,
|
||||
profileConfidence: profiles[0]?.confidence ?? null,
|
||||
},
|
||||
grant: {
|
||||
id: row.grantId,
|
||||
title: row.grantTitle,
|
||||
funder: row.grantFunder,
|
||||
funderEin: row.grantFunderEin,
|
||||
synopsis: row.grantSynopsis,
|
||||
awardFloor: row.grantFloor,
|
||||
awardCeiling: row.grantCeiling,
|
||||
closeDate: row.grantClose,
|
||||
sourceUrl: row.grantSourceUrl,
|
||||
source: row.grantSource,
|
||||
eligibilityEntityTypes: row.grantEligibility,
|
||||
geographicScope: row.grantGeo,
|
||||
applicationEffortEstimate: row.grantEffort,
|
||||
},
|
||||
funderGivingHistory,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user