- Filter scored matches to only show matchups where a manager scores Brackt points (winner drafted) or has a team eliminated (loser drafted); matches with no manager involvement are suppressed entirely. - Escape Discord markdown characters (_*~`|\) in team names and usernames to prevent formatting issues (e.g. double-underscore names causing unintended underlines). - Replace full standings with a "Standings Changes" section that shows only teams whose points changed, plus any teams whose rank shifted as a result, with ↑N / ↓N indicators for rank movement. - Pass previousRanks map from scoring-calculator to the notification so rank-displaced teams (who didn't score points themselves) are included. - Update test webhook in league settings to demonstrate rank changes and a sample scored match. - Update all discord service tests to cover the new filtering, escaping, and standings-change behaviour. https://claude.ai/code/session_01FciwfdG9Sfr5ZrXUHPeB18
This commit is contained in:
parent
52e7c98805
commit
be9f30753f
4 changed files with 191 additions and 64 deletions
|
|
@ -1280,6 +1280,9 @@ export async function recalculateAffectedLeagues(
|
||||||
const previousPointsById = new Map(
|
const previousPointsById = new Map(
|
||||||
beforeStandings.map((s) => [s.teamId, parseFloat(s.totalPoints)])
|
beforeStandings.map((s) => [s.teamId, parseFloat(s.totalPoints)])
|
||||||
);
|
);
|
||||||
|
const previousRanksById = new Map(
|
||||||
|
beforeStandings.map((s) => [s.teamId, s.currentRank])
|
||||||
|
);
|
||||||
|
|
||||||
await recalculateStandings(seasonId, db);
|
await recalculateStandings(seasonId, db);
|
||||||
|
|
||||||
|
|
@ -1391,6 +1394,7 @@ export async function recalculateAffectedLeagues(
|
||||||
seasonName: `${season.league.name} ${season.year}`,
|
seasonName: `${season.league.name} ${season.year}`,
|
||||||
standings,
|
standings,
|
||||||
previousStandings: previousPointsById,
|
previousStandings: previousPointsById,
|
||||||
|
previousRanks: previousRanksById,
|
||||||
sportName,
|
sportName,
|
||||||
eventName: options?.eventName,
|
eventName: options?.eventName,
|
||||||
scoredMatches,
|
scoredMatches,
|
||||||
|
|
|
||||||
|
|
@ -247,7 +247,15 @@ export async function action(args: Route.ActionArgs) {
|
||||||
["2", 125],
|
["2", 125],
|
||||||
["3", 100],
|
["3", 100],
|
||||||
]),
|
]),
|
||||||
|
previousRanks: new Map([
|
||||||
|
["1", 2],
|
||||||
|
["2", 1],
|
||||||
|
["3", 3],
|
||||||
|
]),
|
||||||
eventName: "Test Notification",
|
eventName: "Test Notification",
|
||||||
|
scoredMatches: [
|
||||||
|
{ winnerName: "Team Alpha", loserName: "Team Beta", winnerUsername: "manager1", loserUsername: "manager2" },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
return { testSuccess: true };
|
return { testSuccess: true };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
|
|
@ -52,14 +52,14 @@ describe("sendStandingsUpdateNotification", () => {
|
||||||
webhookUrl: WEBHOOK_URL,
|
webhookUrl: WEBHOOK_URL,
|
||||||
seasonName: "My League 2025",
|
seasonName: "My League 2025",
|
||||||
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 150, rank: 1 }],
|
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 150, rank: 1 }],
|
||||||
previousStandings: new Map(),
|
previousStandings: new Map([["a", 125]]),
|
||||||
});
|
});
|
||||||
|
|
||||||
const body = JSON.parse((fetch as ReturnType<typeof vi.fn>).mock.calls[0][1].body);
|
const body = JSON.parse((fetch as ReturnType<typeof vi.fn>).mock.calls[0][1].body);
|
||||||
expect(body.embeds[0].title).toBe("📊 Standings Update — My League 2025");
|
expect(body.embeds[0].title).toBe("📊 Standings Update — My League 2025");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows point deltas as integers in description", async () => {
|
it("shows point deltas as integers in standings changes", async () => {
|
||||||
await sendStandingsUpdateNotification({
|
await sendStandingsUpdateNotification({
|
||||||
webhookUrl: WEBHOOK_URL,
|
webhookUrl: WEBHOOK_URL,
|
||||||
seasonName: "Test League",
|
seasonName: "Test League",
|
||||||
|
|
@ -77,16 +77,16 @@ describe("sendStandingsUpdateNotification", () => {
|
||||||
expect(desc).toContain("Alpha");
|
expect(desc).toContain("Alpha");
|
||||||
expect(desc).toContain("+25 pts");
|
expect(desc).toContain("+25 pts");
|
||||||
expect(desc).not.toContain("+25.0");
|
expect(desc).not.toContain("+25.0");
|
||||||
// Beta didn't change — no delta shown
|
// Beta didn't change — not shown in standings changes
|
||||||
expect(desc).toMatch(/Beta.*100 pts(?!\s*\()/);
|
expect(desc).not.toContain("Beta");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes sport name and event name in header when both provided", async () => {
|
it("includes sport name and event name in header when both provided", async () => {
|
||||||
await sendStandingsUpdateNotification({
|
await sendStandingsUpdateNotification({
|
||||||
webhookUrl: WEBHOOK_URL,
|
webhookUrl: WEBHOOK_URL,
|
||||||
seasonName: "My League 2025",
|
seasonName: "My League 2025",
|
||||||
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }],
|
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 125, rank: 1 }],
|
||||||
previousStandings: new Map(),
|
previousStandings: new Map([["a", 100]]),
|
||||||
sportName: "UEFA Champions League",
|
sportName: "UEFA Champions League",
|
||||||
eventName: "Knockout Stage",
|
eventName: "Knockout Stage",
|
||||||
});
|
});
|
||||||
|
|
@ -99,8 +99,8 @@ describe("sendStandingsUpdateNotification", () => {
|
||||||
await sendStandingsUpdateNotification({
|
await sendStandingsUpdateNotification({
|
||||||
webhookUrl: WEBHOOK_URL,
|
webhookUrl: WEBHOOK_URL,
|
||||||
seasonName: "My League 2025",
|
seasonName: "My League 2025",
|
||||||
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }],
|
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 125, rank: 1 }],
|
||||||
previousStandings: new Map(),
|
previousStandings: new Map([["a", 100]]),
|
||||||
eventName: "Quarter Finals",
|
eventName: "Quarter Finals",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -113,37 +113,36 @@ describe("sendStandingsUpdateNotification", () => {
|
||||||
await sendStandingsUpdateNotification({
|
await sendStandingsUpdateNotification({
|
||||||
webhookUrl: WEBHOOK_URL,
|
webhookUrl: WEBHOOK_URL,
|
||||||
seasonName: "My League 2025",
|
seasonName: "My League 2025",
|
||||||
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }],
|
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 125, rank: 1 }],
|
||||||
previousStandings: new Map(),
|
previousStandings: new Map([["a", 100]]),
|
||||||
});
|
});
|
||||||
|
|
||||||
const desc = getDescription();
|
const desc = getDescription();
|
||||||
// Description should start directly with the standings section
|
expect(desc).toContain("**Standings Changes**");
|
||||||
expect(desc).toContain("**Current Standings**");
|
|
||||||
expect(desc).not.toContain("**Scored Matches**");
|
expect(desc).not.toContain("**Scored Matches**");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("lists scored matches above standings", async () => {
|
it("lists scored matches above standings changes", async () => {
|
||||||
await sendStandingsUpdateNotification({
|
await sendStandingsUpdateNotification({
|
||||||
webhookUrl: WEBHOOK_URL,
|
webhookUrl: WEBHOOK_URL,
|
||||||
seasonName: "My League 2025",
|
seasonName: "My League 2025",
|
||||||
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }],
|
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 125, rank: 1 }],
|
||||||
previousStandings: new Map(),
|
previousStandings: new Map([["a", 100]]),
|
||||||
scoredMatches: [
|
scoredMatches: [
|
||||||
{ winnerName: "Real Madrid", loserName: "Bayern Munich" },
|
{ winnerName: "Real Madrid", loserName: "Bayern Munich", winnerUsername: "manager1" },
|
||||||
{ winnerName: "Arsenal", loserName: "PSG" },
|
{ winnerName: "Arsenal", loserName: "PSG", loserUsername: "manager2" },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
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 (manager1)** def. Bayern Munich");
|
||||||
expect(desc).toContain("• **Arsenal** def. PSG");
|
expect(desc).toContain("• **Arsenal** def. PSG (manager2)");
|
||||||
// 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("Standings Changes"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows username in parentheses in standings when provided", async () => {
|
it("shows username in parentheses in standings changes when provided", async () => {
|
||||||
await sendStandingsUpdateNotification({
|
await sendStandingsUpdateNotification({
|
||||||
webhookUrl: WEBHOOK_URL,
|
webhookUrl: WEBHOOK_URL,
|
||||||
seasonName: "My League 2025",
|
seasonName: "My League 2025",
|
||||||
|
|
@ -151,21 +150,24 @@ describe("sendStandingsUpdateNotification", () => {
|
||||||
{ teamId: "a", teamName: "Alpha FC", username: "christhrowsrocks", totalPoints: 150, rank: 1 },
|
{ teamId: "a", teamName: "Alpha FC", username: "christhrowsrocks", totalPoints: 150, rank: 1 },
|
||||||
{ teamId: "b", teamName: "Beta United", totalPoints: 100, rank: 2 },
|
{ teamId: "b", teamName: "Beta United", totalPoints: 100, rank: 2 },
|
||||||
],
|
],
|
||||||
previousStandings: new Map(),
|
previousStandings: new Map([
|
||||||
|
["a", 125],
|
||||||
|
["b", 100],
|
||||||
|
]),
|
||||||
});
|
});
|
||||||
|
|
||||||
const desc = getDescription();
|
const desc = getDescription();
|
||||||
expect(desc).toContain("1. Alpha FC (christhrowsrocks) — 150 pts");
|
expect(desc).toContain("1. Alpha FC (christhrowsrocks) — 150 pts");
|
||||||
expect(desc).toContain("2. Beta United — 100 pts");
|
// Beta didn't change points or rank, so it's not shown
|
||||||
expect(desc).not.toContain("Beta United (");
|
expect(desc).not.toContain("Beta United");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows usernames in parentheses in match results when both are drafted", async () => {
|
it("shows usernames in parentheses in match results when both are drafted", async () => {
|
||||||
await sendStandingsUpdateNotification({
|
await sendStandingsUpdateNotification({
|
||||||
webhookUrl: WEBHOOK_URL,
|
webhookUrl: WEBHOOK_URL,
|
||||||
seasonName: "My League 2025",
|
seasonName: "My League 2025",
|
||||||
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }],
|
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 125, rank: 1 }],
|
||||||
previousStandings: new Map(),
|
previousStandings: new Map([["a", 100]]),
|
||||||
scoredMatches: [
|
scoredMatches: [
|
||||||
{
|
{
|
||||||
winnerName: "Sporting",
|
winnerName: "Sporting",
|
||||||
|
|
@ -180,27 +182,27 @@ describe("sendStandingsUpdateNotification", () => {
|
||||||
expect(desc).toContain("• **Sporting (christhrowsrocks)** def. Bodø/Glimt (apatel)");
|
expect(desc).toContain("• **Sporting (christhrowsrocks)** def. Bodø/Glimt (apatel)");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows no parentheses in match results when neither side has a username", async () => {
|
it("omits scored matches where neither side has a username", async () => {
|
||||||
await sendStandingsUpdateNotification({
|
await sendStandingsUpdateNotification({
|
||||||
webhookUrl: WEBHOOK_URL,
|
webhookUrl: WEBHOOK_URL,
|
||||||
seasonName: "My League 2025",
|
seasonName: "My League 2025",
|
||||||
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }],
|
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 125, rank: 1 }],
|
||||||
previousStandings: new Map(),
|
previousStandings: new Map([["a", 100]]),
|
||||||
scoredMatches: [{ winnerName: "Real Madrid", loserName: "Bayern Munich" }],
|
scoredMatches: [{ winnerName: "Real Madrid", loserName: "Bayern Munich" }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const desc = getDescription();
|
const desc = getDescription();
|
||||||
expect(desc).toContain("• **Real Madrid** def. Bayern Munich");
|
expect(desc).not.toContain("**Scored Matches**");
|
||||||
expect(desc).not.toContain("Real Madrid (");
|
expect(desc).not.toContain("Real Madrid");
|
||||||
expect(desc).not.toContain("Bayern Munich (");
|
expect(desc).not.toContain("Bayern Munich");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("omits username parenthesis only for the undrafted side in match results", async () => {
|
it("omits username parenthesis only for the undrafted side in match results", async () => {
|
||||||
await sendStandingsUpdateNotification({
|
await sendStandingsUpdateNotification({
|
||||||
webhookUrl: WEBHOOK_URL,
|
webhookUrl: WEBHOOK_URL,
|
||||||
seasonName: "My League 2025",
|
seasonName: "My League 2025",
|
||||||
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }],
|
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 125, rank: 1 }],
|
||||||
previousStandings: new Map(),
|
previousStandings: new Map([["a", 100]]),
|
||||||
scoredMatches: [
|
scoredMatches: [
|
||||||
{
|
{
|
||||||
winnerName: "Sporting",
|
winnerName: "Sporting",
|
||||||
|
|
@ -215,7 +217,7 @@ describe("sendStandingsUpdateNotification", () => {
|
||||||
expect(desc).not.toContain("Bodø/Glimt (");
|
expect(desc).not.toContain("Bodø/Glimt (");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses plain numbers for all ranks in description", async () => {
|
it("shows teams that changed rank even without a point change", async () => {
|
||||||
await sendStandingsUpdateNotification({
|
await sendStandingsUpdateNotification({
|
||||||
webhookUrl: WEBHOOK_URL,
|
webhookUrl: WEBHOOK_URL,
|
||||||
seasonName: "My League 2025",
|
seasonName: "My League 2025",
|
||||||
|
|
@ -223,27 +225,107 @@ describe("sendStandingsUpdateNotification", () => {
|
||||||
{ teamId: "a", teamName: "Alpha", totalPoints: 150, rank: 1 },
|
{ teamId: "a", teamName: "Alpha", totalPoints: 150, rank: 1 },
|
||||||
{ teamId: "b", teamName: "Beta", totalPoints: 125, rank: 2 },
|
{ teamId: "b", teamName: "Beta", totalPoints: 125, rank: 2 },
|
||||||
{ teamId: "c", teamName: "Gamma", totalPoints: 100, rank: 3 },
|
{ teamId: "c", teamName: "Gamma", totalPoints: 100, rank: 3 },
|
||||||
{ teamId: "d", teamName: "Delta", totalPoints: 75, rank: 4 },
|
|
||||||
],
|
],
|
||||||
previousStandings: new Map(),
|
previousStandings: new Map([
|
||||||
|
["a", 125],
|
||||||
|
["b", 125],
|
||||||
|
["c", 100],
|
||||||
|
]),
|
||||||
|
previousRanks: new Map([
|
||||||
|
["a", 2],
|
||||||
|
["b", 1],
|
||||||
|
["c", 3],
|
||||||
|
]),
|
||||||
});
|
});
|
||||||
|
|
||||||
const desc = getDescription();
|
const desc = getDescription();
|
||||||
expect(desc).toContain("1. Alpha");
|
// Alpha gained points and moved up
|
||||||
expect(desc).toContain("2. Beta");
|
expect(desc).toContain("Alpha");
|
||||||
expect(desc).toContain("3. Gamma");
|
expect(desc).toContain("+25 pts");
|
||||||
expect(desc).toContain("4. Delta");
|
expect(desc).toContain("↑1");
|
||||||
|
// Beta didn't gain points but was displaced — rank changed
|
||||||
|
expect(desc).toContain("Beta");
|
||||||
|
expect(desc).toContain("↓1");
|
||||||
|
// Gamma unchanged — not shown
|
||||||
|
expect(desc).not.toContain("Gamma");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("omits rank delta when no previousRanks provided", async () => {
|
||||||
|
await sendStandingsUpdateNotification({
|
||||||
|
webhookUrl: WEBHOOK_URL,
|
||||||
|
seasonName: "My League 2025",
|
||||||
|
standings: [
|
||||||
|
{ teamId: "a", teamName: "Alpha", totalPoints: 150, rank: 1 },
|
||||||
|
],
|
||||||
|
previousStandings: new Map([["a", 125]]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const desc = getDescription();
|
||||||
|
expect(desc).toContain("Alpha");
|
||||||
|
expect(desc).not.toContain("↑");
|
||||||
|
expect(desc).not.toContain("↓");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("escapes Discord markdown characters in team names and usernames", async () => {
|
||||||
|
await sendStandingsUpdateNotification({
|
||||||
|
webhookUrl: WEBHOOK_URL,
|
||||||
|
seasonName: "My League 2025",
|
||||||
|
standings: [
|
||||||
|
{ teamId: "a", teamName: "Team__Underline", username: "user__name", totalPoints: 150, rank: 1 },
|
||||||
|
],
|
||||||
|
previousStandings: new Map([["a", 125]]),
|
||||||
|
scoredMatches: [
|
||||||
|
{
|
||||||
|
winnerName: "Winner*Bold*",
|
||||||
|
loserName: "Loser~Strike~",
|
||||||
|
winnerUsername: "user_one",
|
||||||
|
loserUsername: "user_two",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const desc = getDescription();
|
||||||
|
expect(desc).toContain("Team\\_\\_Underline");
|
||||||
|
expect(desc).toContain("user\\_\\_name");
|
||||||
|
expect(desc).toContain("Winner\\*Bold\\*");
|
||||||
|
expect(desc).toContain("Loser\\~Strike\\~");
|
||||||
|
expect(desc).toContain("user\\_one");
|
||||||
|
expect(desc).toContain("user\\_two");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not show fields on the embed", async () => {
|
it("does not show fields on the embed", async () => {
|
||||||
await sendStandingsUpdateNotification({
|
await sendStandingsUpdateNotification({
|
||||||
webhookUrl: WEBHOOK_URL,
|
webhookUrl: WEBHOOK_URL,
|
||||||
seasonName: "My League 2025",
|
seasonName: "My League 2025",
|
||||||
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }],
|
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 125, rank: 1 }],
|
||||||
previousStandings: new Map(),
|
previousStandings: new Map([["a", 100]]),
|
||||||
});
|
});
|
||||||
|
|
||||||
const body = JSON.parse((fetch as ReturnType<typeof vi.fn>).mock.calls[0][1].body);
|
const body = JSON.parse((fetch as ReturnType<typeof vi.fn>).mock.calls[0][1].body);
|
||||||
expect(body.embeds[0].fields).toBeUndefined();
|
expect(body.embeds[0].fields).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("omits standings changes section when no teams changed", async () => {
|
||||||
|
await sendStandingsUpdateNotification({
|
||||||
|
webhookUrl: WEBHOOK_URL,
|
||||||
|
seasonName: "My League 2025",
|
||||||
|
standings: [
|
||||||
|
{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 },
|
||||||
|
{ teamId: "b", teamName: "Beta", totalPoints: 75, rank: 2 },
|
||||||
|
],
|
||||||
|
previousStandings: new Map([
|
||||||
|
["a", 100],
|
||||||
|
["b", 75],
|
||||||
|
]),
|
||||||
|
previousRanks: new Map([
|
||||||
|
["a", 1],
|
||||||
|
["b", 2],
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const desc = getDescription();
|
||||||
|
expect(desc).not.toContain("**Standings Changes**");
|
||||||
|
expect(desc).not.toContain("Alpha");
|
||||||
|
expect(desc).not.toContain("Beta");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,11 @@ export async function sendDiscordWebhook(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Escape characters that trigger Discord markdown formatting. */
|
||||||
|
function escapeMarkdown(text: string): string {
|
||||||
|
return text.replace(/[_*~`|\\]/g, "\\$&");
|
||||||
|
}
|
||||||
|
|
||||||
export interface StandingEntry {
|
export interface StandingEntry {
|
||||||
teamId: string;
|
teamId: string;
|
||||||
teamName: string;
|
teamName: string;
|
||||||
|
|
@ -46,6 +51,7 @@ export async function sendStandingsUpdateNotification({
|
||||||
seasonName,
|
seasonName,
|
||||||
standings,
|
standings,
|
||||||
previousStandings,
|
previousStandings,
|
||||||
|
previousRanks,
|
||||||
sportName,
|
sportName,
|
||||||
eventName,
|
eventName,
|
||||||
scoredMatches,
|
scoredMatches,
|
||||||
|
|
@ -54,6 +60,7 @@ export async function sendStandingsUpdateNotification({
|
||||||
seasonName: string;
|
seasonName: string;
|
||||||
standings: StandingEntry[];
|
standings: StandingEntry[];
|
||||||
previousStandings: Map<string, number>;
|
previousStandings: Map<string, number>;
|
||||||
|
previousRanks?: Map<string, number>;
|
||||||
sportName?: string;
|
sportName?: string;
|
||||||
eventName?: string;
|
eventName?: string;
|
||||||
scoredMatches?: ScoredMatch[];
|
scoredMatches?: ScoredMatch[];
|
||||||
|
|
@ -66,32 +73,58 @@ export async function sendStandingsUpdateNotification({
|
||||||
sections.push(`**${parts.join(" — ")}**`);
|
sections.push(`**${parts.join(" — ")}**`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scored matches section
|
// Scored matches section — only show matches where at least one manager
|
||||||
if (scoredMatches && scoredMatches.length > 0) {
|
// (fantasy team owner) scored Brackt points or had a team eliminated.
|
||||||
|
const relevantMatches = scoredMatches?.filter(
|
||||||
|
(m) => m.winnerUsername !== undefined || m.loserUsername !== undefined
|
||||||
|
);
|
||||||
|
if (relevantMatches && relevantMatches.length > 0) {
|
||||||
sections.push("\n**Scored Matches**");
|
sections.push("\n**Scored Matches**");
|
||||||
for (const match of scoredMatches) {
|
for (const match of relevantMatches) {
|
||||||
const winner = match.winnerUsername
|
const winnerLabel = match.winnerUsername
|
||||||
? `${match.winnerName} (${match.winnerUsername})`
|
? `${escapeMarkdown(match.winnerName)} (${escapeMarkdown(match.winnerUsername)})`
|
||||||
: match.winnerName;
|
: escapeMarkdown(match.winnerName);
|
||||||
const loser = match.loserUsername
|
const loserLabel = match.loserUsername
|
||||||
? `${match.loserName} (${match.loserUsername})`
|
? `${escapeMarkdown(match.loserName)} (${escapeMarkdown(match.loserUsername)})`
|
||||||
: match.loserName;
|
: escapeMarkdown(match.loserName);
|
||||||
sections.push(`• **${winner}** def. ${loser}`);
|
sections.push(`• **${winnerLabel}** def. ${loserLabel}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Standings section
|
// Standings changes section — show teams whose points or rank changed.
|
||||||
sections.push("\n**Current Standings**");
|
const changedTeams = standings.filter((s) => {
|
||||||
for (const s of standings) {
|
const prevPoints = previousStandings.get(s.teamId);
|
||||||
const prev = previousStandings.get(s.teamId);
|
const pointsChanged = prevPoints !== undefined && prevPoints !== s.totalPoints;
|
||||||
let delta = "";
|
const prevRank = previousRanks?.get(s.teamId);
|
||||||
if (prev !== undefined && prev !== s.totalPoints) {
|
const rankChanged = prevRank !== undefined && prevRank !== s.rank;
|
||||||
const diff = Math.round(s.totalPoints - prev);
|
return pointsChanged || rankChanged;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (changedTeams.length > 0) {
|
||||||
|
sections.push("\n**Standings Changes**");
|
||||||
|
for (const s of changedTeams) {
|
||||||
|
const prevPoints = previousStandings.get(s.teamId);
|
||||||
|
let pointDelta = "";
|
||||||
|
if (prevPoints !== undefined && prevPoints !== s.totalPoints) {
|
||||||
|
const diff = Math.round(s.totalPoints - prevPoints);
|
||||||
const sign = diff > 0 ? "+" : "";
|
const sign = diff > 0 ? "+" : "";
|
||||||
delta = ` **(${sign}${diff} pts)**`;
|
pointDelta = ` **(${sign}${diff} pts)**`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rankDelta = "";
|
||||||
|
if (previousRanks) {
|
||||||
|
const prevRank = previousRanks.get(s.teamId);
|
||||||
|
if (prevRank !== undefined && prevRank !== s.rank) {
|
||||||
|
const moved = prevRank - s.rank; // positive = moved up
|
||||||
|
rankDelta = moved > 0 ? ` ↑${moved}` : ` ↓${Math.abs(moved)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const escapedName = escapeMarkdown(s.teamName);
|
||||||
|
const escapedUsername = s.username ? escapeMarkdown(s.username) : undefined;
|
||||||
|
const label = escapedUsername ? `${escapedName} (${escapedUsername})` : escapedName;
|
||||||
|
sections.push(`${s.rank}. ${label} — ${Math.round(s.totalPoints)} pts${pointDelta}${rankDelta}`);
|
||||||
}
|
}
|
||||||
const label = s.username ? `${s.teamName} (${s.username})` : s.teamName;
|
|
||||||
sections.push(`${s.rank}. ${label} — ${Math.round(s.totalPoints)} pts${delta}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_DESCRIPTION = 4096;
|
const MAX_DESCRIPTION = 4096;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue