scoring notifications #115
3 changed files with 79 additions and 16 deletions
|
|
@ -59,7 +59,7 @@ function makeDb(existingResult?: { id: string; isPartialScore: boolean }) {
|
|||
};
|
||||
}
|
||||
|
||||
import { processMatchResult, isLoserNotifiable, processPlayoffEvent } from "../scoring-calculator";
|
||||
import { processMatchResult, isLoserNotifiable, isMatchNotable, processPlayoffEvent } from "../scoring-calculator";
|
||||
import { doesLoserAdvance } from "../playoff-match";
|
||||
import { updateProbabilitiesAfterResult } from "~/services/probability-updater";
|
||||
|
||||
|
|
@ -399,6 +399,24 @@ describe("isLoserNotifiable", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("isMatchNotable", () => {
|
||||
it("returns true when winner scored (loser not notifiable)", () => {
|
||||
expect(isMatchNotable(true, false)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when loser is notifiable (winner did not score)", () => {
|
||||
expect(isMatchNotable(false, true)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true when both winner scored and loser is notifiable", () => {
|
||||
expect(isMatchNotable(true, true)).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when winner only advanced (no points) and loser is not notifiable", () => {
|
||||
expect(isMatchNotable(false, false)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("processPlayoffEvent - NBA Play-In Round 1 loserAdvances fix", () => {
|
||||
function makePlayInDb(matches: object[]) {
|
||||
const insertedRows: Array<{ participantId: string; finalPosition: number; isPartialScore: boolean }> = [];
|
||||
|
|
|
|||
|
|
@ -623,6 +623,25 @@ export function isLoserNotifiable(
|
|||
return scoreChanged || finalizedLoserIds.has(loserId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a scored match should be displayed in a Discord standings
|
||||
* notification at all.
|
||||
*
|
||||
* A match is shown only when the winner earned points this round or the loser is
|
||||
* notifiable (eliminated / scored — see {@link isLoserNotifiable}). A drafted
|
||||
* winner that merely advanced without earning points is NOT enough on its own.
|
||||
*
|
||||
* The winner's owner is still credited on any match that passes this gate (even
|
||||
* a points-less advance) — this function only decides whether the match line
|
||||
* appears, not which managers are labelled within it.
|
||||
*/
|
||||
export function isMatchNotable(
|
||||
winnerScoreChanged: boolean,
|
||||
loserNotifiable: boolean
|
||||
): boolean {
|
||||
return winnerScoreChanged || loserNotifiable;
|
||||
}
|
||||
|
||||
export function getGuaranteedMinimumPosition(
|
||||
round: string,
|
||||
bracketTemplateId: string | null | undefined,
|
||||
|
|
@ -1826,12 +1845,13 @@ export async function recalculateAffectedLeagues(
|
|||
);
|
||||
|
||||
// Build scored matches for the notification.
|
||||
// Winners only appear if their team's score changed (they earned points this round).
|
||||
// Losers appear if their team's score changed OR they were definitively eliminated
|
||||
// (finalPosition set, non-partial) — the latter catches 0-pt eliminations that
|
||||
// produce no score delta. Losers who advance to another match (loserAdvances=true,
|
||||
// e.g. NBA 7v8 → PIR2) have no finalized result and no score change, so they're
|
||||
// correctly suppressed.
|
||||
// A match is included only when it is "notable": either the winner earned points this
|
||||
// round OR the loser is notifiable (eliminated / scored). A drafted winner that merely
|
||||
// advanced without earning points is NOT enough on its own (see isMatchNotable).
|
||||
// On any match that IS included, the winning team's owner is always credited — even
|
||||
// when the winner only advanced. Losers appear if their score changed or they were
|
||||
// definitively eliminated (finalPosition set, non-partial); losers who advance to
|
||||
// another match (loserAdvances=true, e.g. NBA 7v8 → PIR2) are correctly suppressed.
|
||||
let scoredMatches: ScoredMatch[] | undefined;
|
||||
if (allCompletedMatches.length > 0) {
|
||||
const relevant = allCompletedMatches.filter(
|
||||
|
|
@ -1857,15 +1877,17 @@ export async function recalculateAffectedLeagues(
|
|||
const winnerOwnerId = lookupOwner(m.winnerId);
|
||||
const loserOwnerId = lookupOwner(m.loserId);
|
||||
const showLoser = isLoserNotifiable(m.loserId, loserTeamId, changedTeamIds, finalizedLoserIds);
|
||||
return {
|
||||
winnerName: m.winnerName ?? "",
|
||||
loserName: m.loserName ?? "",
|
||||
winnerUsername: winnerScoreChanged && winnerOwnerId ? usernameByUserId.get(winnerOwnerId) : undefined,
|
||||
loserUsername: showLoser && loserOwnerId ? usernameByUserId.get(loserOwnerId) : undefined,
|
||||
winnerDiscordUserId: winnerScoreChanged && winnerOwnerId ? discordIdByUserId.get(winnerOwnerId) : undefined,
|
||||
loserDiscordUserId: showLoser && loserOwnerId ? discordIdByUserId.get(loserOwnerId) : undefined,
|
||||
};
|
||||
});
|
||||
return { m, winnerScoreChanged, showLoser, winnerOwnerId, loserOwnerId };
|
||||
})
|
||||
.filter((x) => isMatchNotable(x.winnerScoreChanged, x.showLoser))
|
||||
.map((x) => ({
|
||||
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,
|
||||
winnerDiscordUserId: x.winnerOwnerId ? discordIdByUserId.get(x.winnerOwnerId) : undefined,
|
||||
loserDiscordUserId: x.showLoser && x.loserOwnerId ? discordIdByUserId.get(x.loserOwnerId) : undefined,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -186,6 +186,29 @@ describe("sendStandingsUpdateNotification", () => {
|
|||
expect(desc).toContain("• **Sporting (christhrowsrocks)** def. Bodø/Glimt (apatel)");
|
||||
});
|
||||
|
||||
it("shows winner's owner even when winner only advanced (loser was eliminated)", async () => {
|
||||
// Mirrors the Brazil/Japan scenario: Brazil advanced without earning points,
|
||||
// Japan was eliminated. The calculator now sets winnerUsername unconditionally
|
||||
// on any match that passes the notability gate.
|
||||
await sendStandingsUpdateNotification({
|
||||
webhookUrl: WEBHOOK_URL,
|
||||
seasonName: "Rumble League 2026",
|
||||
standings: [{ teamId: "a", teamName: "Alpha", totalPoints: 100, rank: 1 }],
|
||||
previousStandings: new Map([["a", 100]]),
|
||||
scoredMatches: [
|
||||
{
|
||||
winnerName: "Brazil",
|
||||
loserName: "Japan",
|
||||
winnerUsername: "someManager",
|
||||
loserUsername: "ikyn",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const desc = getDescription();
|
||||
expect(desc).toContain("• **Brazil (someManager)** def. Japan (ikyn)");
|
||||
});
|
||||
|
||||
it("omits scored matches where neither side has a username", async () => {
|
||||
await sendStandingsUpdateNotification({
|
||||
webhookUrl: WEBHOOK_URL,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue