import { normalizeTeamName } from "~/lib/normalize-team-name"; export type ReconciliationParticipant = { id: string; name: string; externalId?: string | null; }; export type MatchConfidence = "exact" | "partial" | "review" | "none"; export type CandidateMatch = { participantId: string; participantName: string; confidence: MatchConfidence; score: number; }; export type UnmatchedTeamResolutionView = { suggestedParticipantId: string | null; suggestedParticipantName: string | null; confidence: MatchConfidence; topCandidates: CandidateMatch[]; orderedParticipants: ReconciliationParticipant[]; }; function tokenSet(value: string): Set { return new Set( normalizeTeamName(value) .split(" ") .map((token) => token.trim()) .filter(Boolean) ); } function reviewScore(apiTeamName: string, participantName: string): number { const normalizedApi = normalizeTeamName(apiTeamName); const normalizedParticipant = normalizeTeamName(participantName); if (!normalizedApi || !normalizedParticipant) return 0; const apiTokens = tokenSet(apiTeamName); const participantTokens = tokenSet(participantName); const sharedTokens = [...apiTokens].filter((token) => participantTokens.has(token)); let score = 0; if (sharedTokens.length > 0) { score += sharedTokens.length * 20; } if (normalizedApi.startsWith(normalizedParticipant) || normalizedParticipant.startsWith(normalizedApi)) { score += 12; } if (normalizedApi[0] === normalizedParticipant[0]) { score += 4; } const lengthDelta = Math.abs(normalizedApi.length - normalizedParticipant.length); score -= Math.min(lengthDelta, 10); return Math.max(score, 0); } export function rankParticipantMatches( apiTeamName: string, participants: ReconciliationParticipant[] ): CandidateMatch[] { const normalizedApi = normalizeTeamName(apiTeamName); return participants .map((participant) => { const normalizedParticipant = normalizeTeamName(participant.name); if (!normalizedApi || !normalizedParticipant) { return { participantId: participant.id, participantName: participant.name, confidence: "none" as const, score: 0, }; } if (normalizedParticipant === normalizedApi) { return { participantId: participant.id, participantName: participant.name, confidence: "exact" as const, score: 100, }; } if ( normalizedApi.length >= 4 && normalizedParticipant.length >= 4 && (normalizedApi.includes(normalizedParticipant) || normalizedParticipant.includes(normalizedApi)) ) { return { participantId: participant.id, participantName: participant.name, confidence: "partial" as const, score: 80, }; } return { participantId: participant.id, participantName: participant.name, confidence: "review" as const, score: reviewScore(apiTeamName, participant.name), }; }) .toSorted((a, b) => b.score - a.score || a.participantName.localeCompare(b.participantName)); } export function buildUnmatchedTeamResolutionView( apiTeamName: string, participants: ReconciliationParticipant[] ): UnmatchedTeamResolutionView { const rankedCandidates = rankParticipantMatches(apiTeamName, participants); const topCandidates = rankedCandidates.filter((candidate) => candidate.score > 0).slice(0, 3); const bestCandidate = rankedCandidates[0]; const confidence = bestCandidate?.confidence === "exact" || bestCandidate?.confidence === "partial" ? bestCandidate.confidence : topCandidates.length > 0 ? "review" : "none"; const suggestedParticipantId = confidence === "exact" || confidence === "partial" ? bestCandidate.participantId : null; const suggestedParticipantName = confidence === "none" ? null : (bestCandidate?.participantName ?? null); const topCandidateIds = new Set(topCandidates.map((candidate) => candidate.participantId)); const orderedParticipants = [ ...topCandidates .map((candidate) => participants.find((participant) => participant.id === candidate.participantId) ?? null ) .filter((participant): participant is ReconciliationParticipant => participant !== null), ...participants.filter((participant) => !topCandidateIds.has(participant.id)), ]; return { suggestedParticipantId, suggestedParticipantName, confidence, topCandidates, orderedParticipants, }; }