diff --git a/app/models/scoring-calculator.ts b/app/models/scoring-calculator.ts index ab0981a..00d19a0 100644 --- a/app/models/scoring-calculator.ts +++ b/app/models/scoring-calculator.ts @@ -1940,8 +1940,10 @@ export async function recalculateAffectedLeagues( // in the World Cup) does not qualify on its own — their owner hasn't earned anything yet. // When a match qualifies because the loser is owned, the winner's manager tag is still // shown for context (who beat them), but the winner is not Discord-pinged. - // Losers who advance to another match (loserAdvances=true, e.g. NBA 7v8 → PIR2) have - // showLoser=false and are correctly suppressed. + // Both managers' tags are always shown for context when their teams are drafted; the + // showLoser flag (isLoserNotifiable) only gates whether the loser is @-pinged — a loser + // who advanced rather than being eliminated (loserAdvances=true, e.g. NBA 7v8 → PIR2, or + // a World Cup semifinal loser) is named but not pinged. let scoredMatches: ScoredMatch[] | undefined; if (allCompletedMatches.length > 0) { const relevant = allCompletedMatches.filter( @@ -1974,7 +1976,12 @@ export async function recalculateAffectedLeagues( winnerName: x.m.winnerName ?? "", loserName: x.m.loserName ?? "", winnerUsername: x.winnerOwnerId ? usernameByUserId.get(x.winnerOwnerId) : undefined, - loserUsername: x.showLoser && x.loserOwnerId ? usernameByUserId.get(x.loserOwnerId) : undefined, + // Show the loser's manager tag whenever their team is drafted, mirroring the + // winner above — even when the loser advances rather than being eliminated + // (World Cup semifinal → 3rd-place playoff, AFL Qualifying Final → Semi Final). + // The @-ping stays gated by showLoser (loserDiscordUserId below): a still-alive + // loser who neither scored nor was eliminated is named for context but not pinged. + loserUsername: x.loserOwnerId ? usernameByUserId.get(x.loserOwnerId) : undefined, winnerDiscordUserId: x.winnerScoreChanged && x.winnerOwnerId ? discordIdByUserId.get(x.winnerOwnerId) : undefined, loserDiscordUserId: x.showLoser && x.loserOwnerId ? discordIdByUserId.get(x.loserOwnerId) : undefined, })) diff --git a/app/services/__tests__/discord.test.ts b/app/services/__tests__/discord.test.ts index 4f02d65..b13f33e 100644 --- a/app/services/__tests__/discord.test.ts +++ b/app/services/__tests__/discord.test.ts @@ -186,6 +186,37 @@ describe("sendStandingsUpdateNotification", () => { expect(desc).toContain("• **Sporting (christhrowsrocks)** def. Bodø/Glimt (apatel)"); }); + it("names a non-eliminated loser for context without @-pinging them", async () => { + // Argentina beats England in the World Cup semifinal. Argentina scored, so it's + // pinged; England advances to the 3rd-place playoff (not eliminated, no points + // change), so its manager is shown by plain username but NOT @-pinged. + await sendStandingsUpdateNotification({ + webhookUrl: WEBHOOK_URL, + seasonName: "Diablo League 2026", + standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 160, rank: 7 }], + previousStandings: new Map([["a", 130]]), + scoredMatches: [ + { + winnerName: "Argentina", + loserName: "England", + winnerUsername: "philosohraptors", + winnerDiscordUserId: "111", + loserUsername: "elementsoul", + // no loserDiscordUserId — still alive, no ping + }, + ], + }); + + const payload = getPayload(); + const desc = payload.embeds[0].description as string; + // Winner scored → rendered as an @-mention; loser is named by plain username. + expect(desc).toContain("• **Argentina (<@111>)** def. England (elementsoul)"); + // England's manager is named but not mentioned/pinged. + expect(desc).not.toContain("England (<@"); + expect(payload.content ?? "").toContain("<@111>"); + expect(payload.content ?? "").not.toContain("elementsoul"); + }); + it("shows winner's manager for context when the match fires due to an owned loser", async () => { // Brazil beats Japan (R32, non-scoring). Japan's manager is the reason for the // notification; Brazil's manager is shown for context even though they didn't score.