Fix Discord notification edge cases from code review

- Fix point delta sign: negative diffs now correctly show -5 pts not +-5 pts
- Replace hardcoded soccer emoji with a neutral bullet (works for all sports)
- Truncate embed description at Discord's 4096-char limit
- Thread eventId through processMatchResult and bracket batch handler so
  scored matches appear in single-match notifications too
- Pass eventName to finalizeQualifyingPoints ("Final Standings") and
  processSeasonStandings ("Season Complete") so those notifications have context
- Test notification now fetches current season to show "League 2025" format

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Chris Parsons 2026-03-17 11:09:49 -07:00
parent 1f8a69a2b7
commit db49c83999
5 changed files with 26 additions and 15 deletions

View file

@ -310,13 +310,14 @@ export async function processMatchResult(
isScoring: boolean; isScoring: boolean;
sportsSeasonId: string; sportsSeasonId: string;
bracketTemplateId?: string | null; bracketTemplateId?: string | null;
eventId?: string;
eventName?: string; eventName?: string;
skipSideEffects?: boolean; skipSideEffects?: boolean;
}, },
providedDb?: ReturnType<typeof database> providedDb?: ReturnType<typeof database>
): Promise<void> { ): Promise<void> {
const db = providedDb || database(); const db = providedDb || database();
const { round, winnerId, loserId, isScoring, sportsSeasonId, bracketTemplateId, eventName, skipSideEffects } = params; const { round, winnerId, loserId, isScoring, sportsSeasonId, bracketTemplateId, eventId, eventName, skipSideEffects } = params;
if (!isScoring) { if (!isScoring) {
// Non-scoring (pre-bracket) round: loser permanently eliminated (0 pts), // Non-scoring (pre-bracket) round: loser permanently eliminated (0 pts),
@ -346,7 +347,7 @@ export async function processMatchResult(
} }
if (!skipSideEffects) { if (!skipSideEffects) {
await recalculateAffectedLeagues(sportsSeasonId, db, eventName ? { eventName } : undefined); await recalculateAffectedLeagues(sportsSeasonId, db, (eventName || eventId) ? { eventName, eventId } : undefined);
try { try {
await updateProbabilitiesAfterResult(sportsSeasonId, true); await updateProbabilitiesAfterResult(sportsSeasonId, true);
} catch (error) { } catch (error) {
@ -629,7 +630,7 @@ export async function finalizeQualifyingPoints(
.where(eq(schema.sportsSeasons.id, sportsSeasonId)); .where(eq(schema.sportsSeasons.id, sportsSeasonId));
// Trigger recalculation for all affected leagues // Trigger recalculation for all affected leagues
await recalculateAffectedLeagues(sportsSeasonId, db); await recalculateAffectedLeagues(sportsSeasonId, db, { eventName: "Final Standings" });
// Auto-trigger probability recalculation after result // Auto-trigger probability recalculation after result
try { try {
@ -739,7 +740,7 @@ export async function processSeasonStandings(
} }
// Trigger recalculation for all affected leagues // Trigger recalculation for all affected leagues
await recalculateAffectedLeagues(sportsSeasonId, db); await recalculateAffectedLeagues(sportsSeasonId, db, { eventName: "Season Complete" });
// Auto-trigger probability recalculation after result // Auto-trigger probability recalculation after result
try { try {
@ -1215,7 +1216,6 @@ export async function recalculateAffectedLeagues(
} }
} }
const standings = afterStandings.map((s) => ({ const standings = afterStandings.map((s) => ({
teamId: s.teamId, teamId: s.teamId,
teamName: s.team.name, teamName: s.team.name,

View file

@ -257,6 +257,7 @@ export async function action({ request, params }: Route.ActionArgs) {
isScoring: match.isScoring ?? true, isScoring: match.isScoring ?? true,
sportsSeasonId: event.sportsSeasonId, sportsSeasonId: event.sportsSeasonId,
bracketTemplateId: event.bracketTemplateId, bracketTemplateId: event.bracketTemplateId,
eventId: event.id,
eventName: event.name ?? undefined, eventName: event.name ?? undefined,
}); });
@ -375,7 +376,7 @@ export async function action({ request, params }: Route.ActionArgs) {
// Run side effects once for the whole batch (avoids N redundant recalcs). // Run side effects once for the whole batch (avoids N redundant recalcs).
if (successCount > 0) { if (successCount > 0) {
const db = database(); const db = database();
await recalculateAffectedLeagues(event.sportsSeasonId, db, event.name ? { eventName: event.name } : undefined); await recalculateAffectedLeagues(event.sportsSeasonId, db, { eventId: event.id, eventName: event.name ?? undefined });
try { try {
await updateProbabilitiesAfterResult(event.sportsSeasonId, true); await updateProbabilitiesAfterResult(event.sportsSeasonId, true);
} catch (error) { } catch (error) {

View file

@ -224,9 +224,11 @@ export async function action(args: Route.ActionArgs) {
return { error: "Invalid Discord webhook URL" }; return { error: "Invalid Discord webhook URL" };
} }
try { try {
const testSeason = await findCurrentSeasonWithSports(leagueId);
const seasonName = testSeason ? `${league.name} ${testSeason.year}` : league.name;
await sendStandingsUpdateNotification({ await sendStandingsUpdateNotification({
webhookUrl, webhookUrl,
seasonName: league.name, seasonName,
standings: [ standings: [
{ teamId: "1", teamName: "Team Alpha", totalPoints: 150, rank: 1 }, { teamId: "1", teamName: "Team Alpha", totalPoints: 150, rank: 1 },
{ teamId: "2", teamName: "Team Beta", totalPoints: 125, rank: 2 }, { teamId: "2", teamName: "Team Beta", totalPoints: 125, rank: 2 },

View file

@ -137,8 +137,8 @@ describe("sendStandingsUpdateNotification", () => {
const desc = getDescription(); const desc = getDescription();
expect(desc).toContain("**Scored Matches**"); expect(desc).toContain("**Scored Matches**");
expect(desc).toContain(" **Real Madrid** def. Bayern Munich"); expect(desc).toContain(" **Real Madrid** def. Bayern Munich");
expect(desc).toContain(" **Arsenal** def. PSG"); expect(desc).toContain(" **Arsenal** def. PSG");
// Scored matches section should appear before standings // Scored matches section should appear before standings
expect(desc.indexOf("Scored Matches")).toBeLessThan(desc.indexOf("Current Standings")); expect(desc.indexOf("Scored Matches")).toBeLessThan(desc.indexOf("Current Standings"));
}); });

View file

@ -69,7 +69,7 @@ export async function sendStandingsUpdateNotification({
if (scoredMatches && scoredMatches.length > 0) { if (scoredMatches && scoredMatches.length > 0) {
sections.push("**Scored Matches**"); sections.push("**Scored Matches**");
for (const match of scoredMatches) { for (const match of scoredMatches) {
sections.push(` **${match.winnerName}** def. ${match.loserName}`); sections.push(` **${match.winnerName}** def. ${match.loserName}`);
} }
} }
@ -77,19 +77,27 @@ export async function sendStandingsUpdateNotification({
sections.push("**Current Standings**"); sections.push("**Current Standings**");
for (const s of standings) { for (const s of standings) {
const prev = previousStandings.get(s.teamId); const prev = previousStandings.get(s.teamId);
const delta = let delta = "";
prev !== undefined && prev !== s.totalPoints if (prev !== undefined && prev !== s.totalPoints) {
? ` **(+${Math.round(s.totalPoints - prev)} pts)**` const diff = Math.round(s.totalPoints - prev);
: ""; const sign = diff > 0 ? "+" : "";
delta = ` **(${sign}${diff} pts)**`;
}
const medal = RANK_MEDALS[s.rank - 1] ?? `${s.rank}.`; const medal = RANK_MEDALS[s.rank - 1] ?? `${s.rank}.`;
sections.push(`${medal} ${s.teamName}${Math.round(s.totalPoints)} pts${delta}`); sections.push(`${medal} ${s.teamName}${Math.round(s.totalPoints)} pts${delta}`);
} }
const MAX_DESCRIPTION = 4096;
let description = sections.join("\n");
if (description.length > MAX_DESCRIPTION) {
description = description.slice(0, MAX_DESCRIPTION - 3) + "...";
}
await sendDiscordWebhook(webhookUrl, { await sendDiscordWebhook(webhookUrl, {
embeds: [ embeds: [
{ {
title: `📊 Standings Update — ${seasonName}`, title: `📊 Standings Update — ${seasonName}`,
description: sections.join("\n"), description,
color: 0x5865f2, // Discord blurple color: 0x5865f2, // Discord blurple
footer: { text: "brackt.com" }, footer: { text: "brackt.com" },
}, },