From 3f6af2665ef55a7f7f210d85288788f782792bad Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Sat, 4 Jul 2026 22:02:06 -0700 Subject: [PATCH] Don't ping owners on rank-only standings changes in Discord The first score of a season displaces every other team's rank (e.g. everyone becomes T2), which fired a mass @-ping of the whole league even though only one owner actually scored. The Standings Changes section pinged every team whose rank *or* points changed. Now the section pings an owner only when that team's points genuinely changed this event; rank-only shufflers are still displayed with their rank delta, but by name and without a mention. Mirrors the existing "display but don't ping" behaviour in the Scored Matches section and preserves pings for pure season-standings sports (F1, IndyCar), where a real scorer's points always change. Co-Authored-By: Claude Opus 4.8 --- app/services/__tests__/discord.test.ts | 37 ++++++++++++++++++++++++++ app/services/discord.ts | 16 +++++++---- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/app/services/__tests__/discord.test.ts b/app/services/__tests__/discord.test.ts index 242a114..786d49a 100644 --- a/app/services/__tests__/discord.test.ts +++ b/app/services/__tests__/discord.test.ts @@ -277,6 +277,43 @@ describe("sendStandingsUpdateNotification", () => { expect(desc).not.toContain("Gamma"); }); + it("pings scorers but not rank-only shufflers", async () => { + await sendStandingsUpdateNotification({ + webhookUrl: WEBHOOK_URL, + seasonName: "My League 2025", + standings: [ + // Alpha scored — points changed, moved up. + { teamId: "a", teamName: "Alpha", totalPoints: 150, rank: 1, discordUserId: "111" }, + // Beta was displaced down without scoring — rank changed only. + { teamId: "b", teamName: "Beta", totalPoints: 125, rank: 2, username: "beta_owner", discordUserId: "222" }, + ], + previousStandings: new Map([ + ["a", 125], + ["b", 125], + ]), + previousRanks: new Map([ + ["a", 2], + ["b", 1], + ]), + }); + + const desc = getDescription(); + // Beta is still displayed with its rank movement… + expect(desc).toContain("Beta"); + expect(desc).toContain("↓1"); + // …but by name, not as an @-mention. + expect(desc).not.toContain("<@222>"); + // Alpha scored, so it keeps its mention. + expect(desc).toContain("<@111>"); + + const payload = getPayload(); + // Only the scorer is pinged. + expect(payload.content).toContain("111"); + expect(payload.content).not.toContain("222"); + expect(payload.allowed_mentions.users).toContain("111"); + expect(payload.allowed_mentions.users).not.toContain("222"); + }); + it("omits rank delta when no previousRanks provided", async () => { await sendStandingsUpdateNotification({ webhookUrl: WEBHOOK_URL, diff --git a/app/services/discord.ts b/app/services/discord.ts index 501ef77..6023bc5 100644 --- a/app/services/discord.ts +++ b/app/services/discord.ts @@ -175,13 +175,19 @@ export async function sendStandingsUpdateNotification({ const isTied = buildTiedRankChecker(standings.map((s) => s.rank)); const rankLabel = (rank: number) => (isTied(rank) ? `T${rank}` : `${rank}`); + // A team's points genuinely changed this event (vs. merely being displaced in + // rank because someone else scored). Only real scorers are pinged; rank-only + // shufflers are still displayed, but by name and without an @-mention. + const pointsChanged = (s: StandingEntry) => { + const prevPoints = previousStandings.get(s.teamId); + return prevPoints !== undefined && prevPoints !== s.totalPoints; + }; + // Standings changes section — show teams whose points or rank changed. const changedTeams = standings.filter((s) => { - const prevPoints = previousStandings.get(s.teamId); - const pointsChanged = prevPoints !== undefined && prevPoints !== s.totalPoints; const prevRank = previousRanks?.get(s.teamId); const rankChanged = prevRank !== undefined && prevRank !== s.rank; - return pointsChanged || rankChanged; + return pointsChanged(s) || rankChanged; }); if (changedTeams.length > 0) { @@ -206,7 +212,7 @@ export async function sendStandingsUpdateNotification({ } const escapedName = escapeMarkdown(s.teamName); - const managerLabel = s.discordUserId + const managerLabel = s.discordUserId && pointsChanged(s) ? `<@${s.discordUserId}>` : s.username ? escapeMarkdown(s.username) @@ -225,7 +231,7 @@ export async function sendStandingsUpdateNotification({ // Collect Discord user IDs of all opted-in managers appearing in this notification. const pingUserIds = new Set(); for (const s of changedTeams) { - if (s.discordUserId) pingUserIds.add(s.discordUserId); + if (s.discordUserId && pointsChanged(s)) pingUserIds.add(s.discordUserId); } for (const m of relevantMatches ?? []) { if (m.winnerDiscordUserId) pingUserIds.add(m.winnerDiscordUserId);