Previously, Game #1 of a multi-game series showed no game number (e.g. "Semifinals") if Game #2 fell outside the calendar date range. Since at least 2 games are always entered for a series from the start, a second un-date-filtered query now checks the max game number per event. If max > 1, the event is a series and Game #1 is labelled accordingly (e.g. "Semifinals Game #1"). https://claude.ai/code/session_01CRrqu43FKHshpJt1NyrDdX
This commit is contained in:
parent
6d6508d503
commit
a4c5e7b4d5
2 changed files with 101 additions and 16 deletions
|
|
@ -142,6 +142,7 @@ describe("getUpcomingEventsForDraftedParticipants — all-compete sports", () =>
|
||||||
|
|
||||||
// ── Bracket patterns ───────────────────────────────────────────────────────
|
// ── Bracket patterns ───────────────────────────────────────────────────────
|
||||||
describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
|
// Main query chain: resolves on orderBy (has leftJoin, orderBy)
|
||||||
function makeSelectChain(rows: unknown[]) {
|
function makeSelectChain(rows: unknown[]) {
|
||||||
const chain = {
|
const chain = {
|
||||||
from: vi.fn().mockReturnThis(),
|
from: vi.fn().mockReturnThis(),
|
||||||
|
|
@ -153,8 +154,21 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
return chain;
|
return chain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Second query chain (max game lookup): resolves on where (no leftJoin, no orderBy)
|
||||||
|
function makeMaxGameChain(rows: unknown[]) {
|
||||||
|
const chain = {
|
||||||
|
from: vi.fn().mockReturnThis(),
|
||||||
|
innerJoin: vi.fn().mockReturnThis(),
|
||||||
|
where: vi.fn().mockResolvedValue(rows),
|
||||||
|
};
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
// Default for the second selectDistinct call (max-game-number lookup).
|
||||||
|
// Individual tests override the first call with mockReturnValueOnce.
|
||||||
|
mockDb.selectDistinct.mockReturnValue(makeMaxGameChain([]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns [] when draftedParticipants is empty", async () => {
|
it("returns [] when draftedParticipants is empty", async () => {
|
||||||
|
|
@ -172,7 +186,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
participant1Id: "man-city", participant2Id: "bayern",
|
participant1Id: "man-city", participant2Id: "bayern",
|
||||||
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
||||||
}];
|
}];
|
||||||
mockDb.selectDistinct.mockReturnValue(makeSelectChain(rows));
|
mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows));
|
||||||
|
|
||||||
const result = await getUpcomingEventsForDraftedParticipants(
|
const result = await getUpcomingEventsForDraftedParticipants(
|
||||||
SPORTS_SEASON_ID, "playoff_bracket",
|
SPORTS_SEASON_ID, "playoff_bracket",
|
||||||
|
|
@ -197,7 +211,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
mockDb.selectDistinct.mockReturnValue(makeSelectChain(rows));
|
mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows));
|
||||||
|
|
||||||
const result = await getUpcomingEventsForDraftedParticipants(
|
const result = await getUpcomingEventsForDraftedParticipants(
|
||||||
SPORTS_SEASON_ID, "playoff_bracket",
|
SPORTS_SEASON_ID, "playoff_bracket",
|
||||||
|
|
@ -227,7 +241,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
mockDb.selectDistinct.mockReturnValue(makeSelectChain(rows));
|
mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows));
|
||||||
|
|
||||||
const result = await getUpcomingEventsForDraftedParticipants(
|
const result = await getUpcomingEventsForDraftedParticipants(
|
||||||
SPORTS_SEASON_ID, "playoff_bracket",
|
SPORTS_SEASON_ID, "playoff_bracket",
|
||||||
|
|
@ -247,7 +261,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
participant1Id: "man-city", participant2Id: "real-madrid",
|
participant1Id: "man-city", participant2Id: "real-madrid",
|
||||||
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
||||||
}];
|
}];
|
||||||
mockDb.selectDistinct.mockReturnValue(makeSelectChain(rows));
|
mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows));
|
||||||
|
|
||||||
const result = await getUpcomingEventsForDraftedParticipants(
|
const result = await getUpcomingEventsForDraftedParticipants(
|
||||||
SPORTS_SEASON_ID, "playoff_bracket",
|
SPORTS_SEASON_ID, "playoff_bracket",
|
||||||
|
|
@ -261,7 +275,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("works for ucl_bracket pattern", async () => {
|
it("works for ucl_bracket pattern", async () => {
|
||||||
mockDb.selectDistinct.mockReturnValue(makeSelectChain([{
|
mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain([{
|
||||||
id: "e1", name: "UCL SF", eventDate: "2025-04-29",
|
id: "e1", name: "UCL SF", eventDate: "2025-04-29",
|
||||||
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
||||||
participant1Id: "man-city", participant2Id: "arsenal",
|
participant1Id: "man-city", participant2Id: "arsenal",
|
||||||
|
|
@ -286,7 +300,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
participant1Id: "barcelona", participant2Id: "sporting",
|
participant1Id: "barcelona", participant2Id: "sporting",
|
||||||
round: "Knockout Stage", gameNumber: 1, scheduledAt: null,
|
round: "Knockout Stage", gameNumber: 1, scheduledAt: null,
|
||||||
}];
|
}];
|
||||||
mockDb.selectDistinct.mockReturnValue(makeSelectChain(rows));
|
mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows));
|
||||||
|
|
||||||
const result = await getUpcomingEventsForDraftedParticipants(
|
const result = await getUpcomingEventsForDraftedParticipants(
|
||||||
SPORTS_SEASON_ID, "playoff_bracket",
|
SPORTS_SEASON_ID, "playoff_bracket",
|
||||||
|
|
@ -309,7 +323,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
participant1Id: "barcelona", participant2Id: "sporting",
|
participant1Id: "barcelona", participant2Id: "sporting",
|
||||||
round: "Knockout Stage", gameNumber: 1, scheduledAt: gameTime,
|
round: "Knockout Stage", gameNumber: 1, scheduledAt: gameTime,
|
||||||
}];
|
}];
|
||||||
mockDb.selectDistinct.mockReturnValue(makeSelectChain(rows));
|
mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows));
|
||||||
|
|
||||||
const result = await getUpcomingEventsForDraftedParticipants(
|
const result = await getUpcomingEventsForDraftedParticipants(
|
||||||
SPORTS_SEASON_ID, "playoff_bracket",
|
SPORTS_SEASON_ID, "playoff_bracket",
|
||||||
|
|
@ -338,7 +352,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
round: "Knockout Stage", gameNumber: 2, scheduledAt: gameTime1,
|
round: "Knockout Stage", gameNumber: 2, scheduledAt: gameTime1,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
mockDb.selectDistinct.mockReturnValue(makeSelectChain(rows));
|
mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows));
|
||||||
|
|
||||||
const result = await getUpcomingEventsForDraftedParticipants(
|
const result = await getUpcomingEventsForDraftedParticipants(
|
||||||
SPORTS_SEASON_ID, "playoff_bracket",
|
SPORTS_SEASON_ID, "playoff_bracket",
|
||||||
|
|
@ -357,7 +371,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
participant1Id: "man-city", participant2Id: "arsenal",
|
participant1Id: "man-city", participant2Id: "arsenal",
|
||||||
round: "Semifinals", gameNumber: 1, scheduledAt: null,
|
round: "Semifinals", gameNumber: 1, scheduledAt: null,
|
||||||
}];
|
}];
|
||||||
mockDb.selectDistinct.mockReturnValue(makeSelectChain(rows));
|
mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows));
|
||||||
|
|
||||||
const result = await getUpcomingEventsForDraftedParticipants(
|
const result = await getUpcomingEventsForDraftedParticipants(
|
||||||
SPORTS_SEASON_ID, "playoff_bracket",
|
SPORTS_SEASON_ID, "playoff_bracket",
|
||||||
|
|
@ -377,7 +391,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
participant1Id: "barcelona", participant2Id: "sporting",
|
participant1Id: "barcelona", participant2Id: "sporting",
|
||||||
round: "Knockout Stage", gameNumber: 1, scheduledAt: null,
|
round: "Knockout Stage", gameNumber: 1, scheduledAt: null,
|
||||||
}];
|
}];
|
||||||
mockDb.selectDistinct.mockReturnValue(makeSelectChain(rows));
|
mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows));
|
||||||
|
|
||||||
const result = await getUpcomingEventsForDraftedParticipants(
|
const result = await getUpcomingEventsForDraftedParticipants(
|
||||||
SPORTS_SEASON_ID, "playoff_bracket",
|
SPORTS_SEASON_ID, "playoff_bracket",
|
||||||
|
|
@ -395,7 +409,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
participant1Id: "man-city", participant2Id: "arsenal",
|
participant1Id: "man-city", participant2Id: "arsenal",
|
||||||
round: "Semifinals", gameNumber: 2, scheduledAt: null,
|
round: "Semifinals", gameNumber: 2, scheduledAt: null,
|
||||||
}];
|
}];
|
||||||
mockDb.selectDistinct.mockReturnValue(makeSelectChain(rows));
|
mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows));
|
||||||
|
|
||||||
const result = await getUpcomingEventsForDraftedParticipants(
|
const result = await getUpcomingEventsForDraftedParticipants(
|
||||||
SPORTS_SEASON_ID, "playoff_bracket",
|
SPORTS_SEASON_ID, "playoff_bracket",
|
||||||
|
|
@ -406,6 +420,32 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
expect(result[0].matchLabel).toBe("Semifinals Game #2");
|
expect(result[0].matchLabel).toBe("Semifinals Game #2");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows Game #1 label when game #1 is upcoming but series has multiple games", async () => {
|
||||||
|
// Game #2 exists in DB but is outside the date window — only Game #1 appears
|
||||||
|
// in the main query. The second (un-date-filtered) query reveals it's a series.
|
||||||
|
const mainRows = [{
|
||||||
|
id: "e1", name: "NBA Finals", eventDate: "2025-04-10",
|
||||||
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
||||||
|
participant1Id: "lakers", participant2Id: "celtics",
|
||||||
|
round: "Finals", gameNumber: 1, scheduledAt: null,
|
||||||
|
}];
|
||||||
|
const allGameRows = [
|
||||||
|
{ eventId: "e1", gameNumber: 1 },
|
||||||
|
{ eventId: "e1", gameNumber: 2 },
|
||||||
|
];
|
||||||
|
mockDb.selectDistinct
|
||||||
|
.mockReturnValueOnce(makeSelectChain(mainRows))
|
||||||
|
.mockReturnValueOnce(makeMaxGameChain(allGameRows));
|
||||||
|
|
||||||
|
const result = await getUpcomingEventsForDraftedParticipants(
|
||||||
|
SPORTS_SEASON_ID, "playoff_bracket",
|
||||||
|
[makeParticipant("lakers", "Lakers")],
|
||||||
|
DATE_FROM, DATE_TO
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result[0].matchLabel).toBe("Finals Game #1");
|
||||||
|
});
|
||||||
|
|
||||||
it("all-compete events have null earliestGameTime and matchLabel", async () => {
|
it("all-compete events have null earliestGameTime and matchLabel", async () => {
|
||||||
const participants = [makeParticipant("p1", "Verstappen")];
|
const participants = [makeParticipant("p1", "Verstappen")];
|
||||||
mockDb.query.scoringEvents.findMany.mockResolvedValue([
|
mockDb.query.scoringEvents.findMany.mockResolvedValue([
|
||||||
|
|
@ -504,11 +544,21 @@ describe("getUpcomingEventsForDraftedParticipants — bracket misc", () => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeMaxGameChain(rows: unknown[]) {
|
||||||
|
return {
|
||||||
|
from: vi.fn().mockReturnThis(),
|
||||||
|
innerJoin: vi.fn().mockReturnThis(),
|
||||||
|
where: vi.fn().mockResolvedValue(rows),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => vi.clearAllMocks());
|
beforeEach(() => vi.clearAllMocks());
|
||||||
|
|
||||||
it("uses leftJoin so events without any games are still considered", async () => {
|
it("uses leftJoin so events without any games are still considered", async () => {
|
||||||
const chain = makeSelectChain([]);
|
const chain = makeSelectChain([]);
|
||||||
mockDb.selectDistinct.mockReturnValue(chain);
|
mockDb.selectDistinct
|
||||||
|
.mockReturnValueOnce(chain)
|
||||||
|
.mockReturnValueOnce(makeMaxGameChain([]));
|
||||||
|
|
||||||
await getUpcomingEventsForDraftedParticipants(
|
await getUpcomingEventsForDraftedParticipants(
|
||||||
SPORTS_SEASON_ID, "playoff_bracket",
|
SPORTS_SEASON_ID, "playoff_bracket",
|
||||||
|
|
|
||||||
|
|
@ -520,6 +520,35 @@ export async function getUpcomingEventsForDraftedParticipants(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine max game number per event across ALL games (not date-filtered)
|
||||||
|
// so we can distinguish "Game #1 of a series" from a single-game matchup.
|
||||||
|
const maxGameNumberByEvent = new Map<string, number>();
|
||||||
|
const eventIds = Array.from(eventMap.keys());
|
||||||
|
if (eventIds.length > 0) {
|
||||||
|
const allGameRows = await db
|
||||||
|
.selectDistinct({
|
||||||
|
eventId: schema.scoringEvents.id,
|
||||||
|
gameNumber: schema.playoffMatchGames.gameNumber,
|
||||||
|
})
|
||||||
|
.from(schema.scoringEvents)
|
||||||
|
.innerJoin(
|
||||||
|
schema.playoffMatches,
|
||||||
|
eq(schema.playoffMatches.scoringEventId, schema.scoringEvents.id)
|
||||||
|
)
|
||||||
|
.innerJoin(
|
||||||
|
schema.playoffMatchGames,
|
||||||
|
eq(schema.playoffMatchGames.playoffMatchId, schema.playoffMatches.id)
|
||||||
|
)
|
||||||
|
.where(inArray(schema.scoringEvents.id, eventIds));
|
||||||
|
|
||||||
|
for (const row of allGameRows) {
|
||||||
|
if (row.gameNumber !== null) {
|
||||||
|
const current = maxGameNumberByEvent.get(row.eventId) ?? 0;
|
||||||
|
maxGameNumberByEvent.set(row.eventId, Math.max(current, row.gameNumber));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const [eventId, event] of eventMap) {
|
for (const [eventId, event] of eventMap) {
|
||||||
event.relevantParticipants = Array.from(participantsByEvent.get(eventId)?.values() ?? []);
|
event.relevantParticipants = Array.from(participantsByEvent.get(eventId)?.values() ?? []);
|
||||||
|
|
||||||
|
|
@ -532,15 +561,21 @@ export async function getUpcomingEventsForDraftedParticipants(
|
||||||
const [round, gameNumberStr] = [...gameKeys][0].split("|");
|
const [round, gameNumberStr] = [...gameKeys][0].split("|");
|
||||||
const gameNumber = parseInt(gameNumberStr, 10);
|
const gameNumber = parseInt(gameNumberStr, 10);
|
||||||
if (gameNumber > 1) {
|
if (gameNumber > 1) {
|
||||||
// Game #2+ indicates a multi-game series — show the game number so
|
// Game #2+ in the date window — always show the game number.
|
||||||
// users can tell which game in the series is upcoming.
|
|
||||||
event.matchLabel =
|
event.matchLabel =
|
||||||
round === event.name
|
round === event.name
|
||||||
? `Game #${gameNumberStr}`
|
? `Game #${gameNumberStr}`
|
||||||
: `${round} Game #${gameNumberStr}`;
|
: `${round} Game #${gameNumberStr}`;
|
||||||
} else {
|
} else {
|
||||||
// Single game (game #1 only) — no number needed, just show the round.
|
// Game #1 — only show game number if this is a multi-game series
|
||||||
event.matchLabel = round === event.name ? null : round;
|
// (i.e. Game #2+ exists in the DB, even if outside the date window).
|
||||||
|
const maxGameNumber = maxGameNumberByEvent.get(eventId) ?? 1;
|
||||||
|
if (maxGameNumber > 1) {
|
||||||
|
event.matchLabel = round === event.name ? `Game #1` : `${round} Game #1`;
|
||||||
|
} else {
|
||||||
|
// Truly a single-game matchup — no number needed.
|
||||||
|
event.matchLabel = round === event.name ? null : round;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (gameKeys.size > 1) {
|
} else if (gameKeys.size > 1) {
|
||||||
// Multiple games across the matchup — use just the round name.
|
// Multiple games across the matchup — use just the round name.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue