Refactor Discord notification logic in scoring calculator

- Compute changedTeamIds once up front; derive hasChanges from it
  instead of duplicating the same iteration
- Merge usernameForParticipant and winnerUsernameForParticipant into a
  single function with a requireScoreChange flag
- Replace hasDraftedParticipantMatches with hasScoredMatchesToShow,
  which checks that at least one scoredMatch has a displayable username
  — prevents sending a contentless Discord embed when a drafted winner
  doesn't score any points and the loser isn't drafted
- Update inline comment to be sport-agnostic

https://claude.ai/code/session_01FciwfdG9Sfr5ZrXUHPeB18
This commit is contained in:
Claude 2026-03-19 22:33:18 +00:00
parent b0c43ef2d3
commit 7c25852dfa
No known key found for this signature in database

View file

@ -1299,10 +1299,15 @@ export async function recalculateAffectedLeagues(
orderBy: (ts, { asc }) => [asc(ts.currentRank)], orderBy: (ts, { asc }) => [asc(ts.currentRank)],
}); });
const hasChanges = afterStandings.some((s) => { const changedTeamIds = new Set(
afterStandings
.filter((s) => {
const prev = previousPointsById.get(s.teamId); const prev = previousPointsById.get(s.teamId);
return prev === undefined || prev !== parseFloat(s.totalPoints); return prev === undefined || prev !== parseFloat(s.totalPoints);
}); })
.map((s) => s.teamId)
);
const hasChanges = changedTeamIds.size > 0;
// Look up the league's Discord webhook URL via the season // Look up the league's Discord webhook URL via the season
const season = await db.query.seasons.findFirst({ const season = await db.query.seasons.findFirst({
@ -1333,12 +1338,10 @@ export async function recalculateAffectedLeagues(
afterStandings.map((s) => [s.teamId, s.team.ownerId]) afterStandings.map((s) => [s.teamId, s.team.ownerId])
); );
// Filter matches to only those involving participants drafted in this season. // Build scored matches for the notification — computed before the gate
// We compute this BEFORE the hasChanges gate because 0-point eliminations // because an elimination (0 points, no score change) still warrants a
// (e.g. R64 losers whose finalPosition=0 doesn't affect team scores) still // notification when a drafted participant is knocked out.
// warrant a Discord notification when a drafted team is eliminated.
let scoredMatches: ScoredMatch[] | undefined; let scoredMatches: ScoredMatch[] | undefined;
let hasDraftedParticipantMatches = false;
if (allCompletedMatches.length > 0) { if (allCompletedMatches.length > 0) {
const draftPicks = await db.query.draftPicks.findMany({ const draftPicks = await db.query.draftPicks.findMany({
where: eq(schema.draftPicks.seasonId, seasonId), where: eq(schema.draftPicks.seasonId, seasonId),
@ -1351,40 +1354,22 @@ export async function recalculateAffectedLeagues(
(m.loserId && draftedIds.has(m.loserId)) (m.loserId && draftedIds.has(m.loserId))
); );
hasDraftedParticipantMatches = relevant.length > 0;
if (relevant.length > 0) { if (relevant.length > 0) {
const teamIdByParticipantId = new Map( const teamIdByParticipantId = new Map(
draftPicks.map((p) => [p.participantId, p.teamId]) draftPicks.map((p) => [p.participantId, p.teamId])
); );
// Teams whose score actually changed after recalculation. // Look up the fantasy owner's display name for a participant.
const changedTeamIds = new Set( // Pass requireScoreChange=true for the winner to avoid surfacing wins
afterStandings // that didn't award points this round.
.filter((s) => { const usernameForParticipant = (
const prev = previousPointsById.get(s.teamId); participantId: string | null | undefined,
return prev === undefined || prev !== parseFloat(s.totalPoints); requireScoreChange = false
}) ) => {
.map((s) => s.teamId)
);
const usernameForParticipant = (participantId: string | null | undefined) => {
if (!participantId) return undefined; if (!participantId) return undefined;
const teamId = teamIdByParticipantId.get(participantId); const teamId = teamIdByParticipantId.get(participantId);
if (!teamId) return undefined; if (!teamId) return undefined;
const ownerId = ownerIdByTeamId.get(teamId); if (requireScoreChange && !changedTeamIds.has(teamId)) return undefined;
if (!ownerId) return undefined;
return usernameByClerkId.get(ownerId);
};
// Only set winnerUsername if the winner's owner actually scored points
// this round. This prevents listing matches where a drafted team won
// but the scoring system didn't award points yet (e.g. R64 wins when
// points only trigger at Sweet 16+).
const winnerUsernameForParticipant = (participantId: string | null | undefined) => {
if (!participantId) return undefined;
const teamId = teamIdByParticipantId.get(participantId);
if (!teamId || !changedTeamIds.has(teamId)) return undefined;
const ownerId = ownerIdByTeamId.get(teamId); const ownerId = ownerIdByTeamId.get(teamId);
if (!ownerId) return undefined; if (!ownerId) return undefined;
return usernameByClerkId.get(ownerId); return usernameByClerkId.get(ownerId);
@ -1395,14 +1380,17 @@ export async function recalculateAffectedLeagues(
.map((m) => ({ .map((m) => ({
winnerName: m.winnerName!, winnerName: m.winnerName!,
loserName: m.loserName!, loserName: m.loserName!,
winnerUsername: winnerUsernameForParticipant(m.winnerId), winnerUsername: usernameForParticipant(m.winnerId, true),
loserUsername: usernameForParticipant(m.loserId), loserUsername: usernameForParticipant(m.loserId),
})); }));
} }
} }
// Skip notification if scores didn't change AND no drafted participants were scored. // Skip notification if scores didn't change AND no match has a displayable username.
if (!hasChanges && !hasDraftedParticipantMatches) continue; const hasScoredMatchesToShow = scoredMatches?.some(
(m) => m.winnerUsername !== undefined || m.loserUsername !== undefined
);
if (!hasChanges && !hasScoredMatchesToShow) continue;
const standings = afterStandings.map((s) => ({ const standings = afterStandings.map((s) => ({
teamId: s.teamId, teamId: s.teamId,