From e03bdd538f4ab55cba406d8021b701003dcc0d48 Mon Sep 17 00:00:00 2001 From: Chris Parsons <438676+chrisparsons83@users.noreply.github.com> Date: Thu, 16 Apr 2026 14:19:59 -0700 Subject: [PATCH] Fix upcoming events grouping to separate individual bracket matches (#302) Include playoffMatchId in the grouping key so that each match within a scoring event (e.g. different Round of 32 matchups in snooker) appears as its own calendar entry instead of being merged together. --- .../__tests__/upcoming-calendar.test.ts | 71 +++++++++++++++++-- app/models/scoring-event.ts | 5 +- 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/app/models/__tests__/upcoming-calendar.test.ts b/app/models/__tests__/upcoming-calendar.test.ts index b8212fa..609ea7a 100644 --- a/app/models/__tests__/upcoming-calendar.test.ts +++ b/app/models/__tests__/upcoming-calendar.test.ts @@ -23,7 +23,7 @@ vi.mock("~/database/context", () => ({ vi.mock("~/database/schema", () => ({ scoringEvents: { sportsSeasonId: "se.sports_season_id", isComplete: "se.is_complete", eventDate: "se.event_date", eventType: "se.event_type" }, - playoffMatches: { scoringEventId: "pm.scoring_event_id", participant1Id: "pm.participant1_id", participant2Id: "pm.participant2_id", round: "pm.round", matchNumber: "pm.match_number", isComplete: "pm.is_complete" }, + playoffMatches: { id: "pm.id", scoringEventId: "pm.scoring_event_id", participant1Id: "pm.participant1_id", participant2Id: "pm.participant2_id", round: "pm.round", matchNumber: "pm.match_number", isComplete: "pm.is_complete" }, playoffMatchGames: { playoffMatchId: "pmg.playoff_match_id", scheduledAt: "pmg.scheduled_at", gameNumber: "pmg.game_number" }, })); @@ -183,6 +183,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => { const rows = [{ id: "e1", name: "UCL QF", eventDate: "2025-04-09", eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, + playoffMatchId: "pm1", participant1Id: "man-city", participant2Id: "bayern", round: "Quarterfinals", gameNumber: 1, scheduledAt: null, }]; @@ -207,6 +208,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => { { id: "e1", name: "UCL QF", eventDate: "2025-04-09", eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, + playoffMatchId: "pm1", participant1Id: "man-city", participant2Id: "arsenal", round: "Quarterfinals", gameNumber: 1, scheduledAt: null, }, @@ -226,17 +228,19 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => { expect(names).toContain("Arsenal"); }); - it("returns multiple events when picks are in different matches", async () => { + it("returns separate events when picks are in different matches within the same scoring event", async () => { const rows = [ { id: "e1", name: "UCL QF", eventDate: "2025-04-09", eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, + playoffMatchId: "pm1", participant1Id: "man-city", participant2Id: "real-madrid", round: "Quarterfinals", gameNumber: 1, scheduledAt: null, }, { id: "e1", name: "UCL QF", eventDate: "2025-04-09", eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, + playoffMatchId: "pm2", participant1Id: "arsenal", participant2Id: "psg", round: "Quarterfinals", gameNumber: 1, scheduledAt: null, }, @@ -249,15 +253,21 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => { DATE_FROM, DATE_TO ); - // Both matches are in the same event → one result with both picks - expect(result).toHaveLength(1); - expect(result[0].relevantParticipants).toHaveLength(2); + // Each match is now a separate entry + expect(result).toHaveLength(2); + const manCityEntry = result.find((r) => r.relevantParticipants.some((p) => p.id === "man-city")); + const arsenalEntry = result.find((r) => r.relevantParticipants.some((p) => p.id === "arsenal")); + expect(manCityEntry).toBeDefined(); + expect(arsenalEntry).toBeDefined(); + expect(manCityEntry?.relevantParticipants).toHaveLength(1); + expect(arsenalEntry?.relevantParticipants).toHaveLength(1); }); it("only includes drafted participants in relevantParticipants, not opponents", async () => { const rows = [{ id: "e1", name: "UCL QF", eventDate: "2025-04-09", eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, + playoffMatchId: "pm1", participant1Id: "man-city", participant2Id: "real-madrid", round: "Quarterfinals", gameNumber: 1, scheduledAt: null, }]; @@ -278,6 +288,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => { mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain([{ id: "e1", name: "UCL SF", eventDate: "2025-04-29", eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, + playoffMatchId: "pm1", participant1Id: "man-city", participant2Id: "arsenal", round: "Semifinals", gameNumber: 1, scheduledAt: null, }])); @@ -297,6 +308,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => { id: "e1", name: "UCL QF", eventDate: null, // ← no date on the event itself eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, + playoffMatchId: "pm1", participant1Id: "barcelona", participant2Id: "sporting", round: "Knockout Stage", gameNumber: 1, scheduledAt: null, }]; @@ -320,6 +332,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => { id: "e1", name: "UCL QF", eventDate: null, eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, + playoffMatchId: "pm1", participant1Id: "barcelona", participant2Id: "sporting", round: "Knockout Stage", gameNumber: 1, scheduledAt: gameTime, }]; @@ -342,12 +355,14 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => { { id: "e1", name: "UCL QF", eventDate: null, eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, + playoffMatchId: "pm1", participant1Id: "barcelona", participant2Id: "sporting", round: "Knockout Stage", gameNumber: 1, scheduledAt: gameTime1, }, { id: "e1", name: "UCL QF", eventDate: null, eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, + playoffMatchId: "pm1", participant1Id: "barcelona", participant2Id: "sporting", round: "Knockout Stage", gameNumber: 2, scheduledAt: gameTime2, }, @@ -366,8 +381,8 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => { ); expect(result).toHaveLength(2); - const game1 = result.find((r) => r.id === "e1|1"); - const game2 = result.find((r) => r.id === "e1|2"); + const game1 = result.find((r) => r.id === "e1|pm1|1"); + const game2 = result.find((r) => r.id === "e1|pm1|2"); expect(game1?.earliestGameTime).toBe(gameTime1.toISOString()); expect(game1?.matchLabel).toBe("Knockout Stage Game #1"); expect(game2?.earliestGameTime).toBe(gameTime2.toISOString()); @@ -378,6 +393,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => { const rows = [{ id: "e1", name: "PDC World Championship", eventDate: "2025-04-09", eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, + playoffMatchId: "pm1", participant1Id: "man-city", participant2Id: "arsenal", round: "Semifinals", gameNumber: 1, scheduledAt: null, }]; @@ -398,6 +414,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => { const rows = [{ id: "e1", name: "Knockout Stage", eventDate: null, eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, + playoffMatchId: "pm1", participant1Id: "barcelona", participant2Id: "sporting", round: "Knockout Stage", gameNumber: 1, scheduledAt: null, }]; @@ -416,6 +433,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => { const rows = [{ id: "e1", name: "PDC World Championship", eventDate: "2025-04-10", eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, + playoffMatchId: "pm1", participant1Id: "man-city", participant2Id: "arsenal", round: "Semifinals", gameNumber: 2, scheduledAt: null, }]; @@ -441,6 +459,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => { const mainRows = [{ id: "e1", name: "NBA Finals", eventDate: "2025-04-10", eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, + playoffMatchId: "pm1", participant1Id: "lakers", participant2Id: "celtics", round: "Finals", gameNumber: 1, scheduledAt: null, }]; @@ -461,6 +480,44 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => { expect(result[0].matchLabel).toBe("Finals Game #1"); }); + it("separates matches within the same scoring event by scheduled time (snooker-style bracket)", async () => { + const time1 = new Date("2025-04-18T06:30:00.000Z"); + const time2 = new Date("2025-04-19T06:30:00.000Z"); + const rows = [ + { + id: "e1", name: "Snooker World Championship", eventDate: null, + eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, + playoffMatchId: "pm-williams", + participant1Id: "mark-williams", participant2Id: "opponent-a", + round: "Round of 32", gameNumber: 1, scheduledAt: time1, + }, + { + id: "e1", name: "Snooker World Championship", eventDate: null, + eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID, + playoffMatchId: "pm-higgins", + participant1Id: "john-higgins", participant2Id: "opponent-b", + round: "Round of 32", gameNumber: 1, scheduledAt: time2, + }, + ]; + mockDb.selectDistinct.mockReturnValueOnce(makeSelectChain(rows)); + + const result = await getUpcomingEventsForDraftedParticipants( + SPORTS_SEASON_ID, "playoff_bracket", + [makeParticipant("mark-williams", "Mark Williams"), makeParticipant("john-higgins", "John Higgins")], + DATE_FROM, DATE_TO + ); + + expect(result).toHaveLength(2); + const williamsEntry = result.find((r) => r.relevantParticipants.some((p) => p.id === "mark-williams")); + const higginsEntry = result.find((r) => r.relevantParticipants.some((p) => p.id === "john-higgins")); + expect(williamsEntry).toBeDefined(); + expect(higginsEntry).toBeDefined(); + expect(williamsEntry?.earliestGameTime).toBe(time1.toISOString()); + expect(higginsEntry?.earliestGameTime).toBe(time2.toISOString()); + expect(williamsEntry?.matchLabel).toBe("Round of 32"); + expect(higginsEntry?.matchLabel).toBe("Round of 32"); + }); + it("all-compete events have null earliestGameTime and matchLabel", async () => { const participants = [makeParticipant("p1", "Verstappen")]; mockDb.query.scoringEvents.findMany.mockResolvedValue([ diff --git a/app/models/scoring-event.ts b/app/models/scoring-event.ts index 1c900b3..a11076e 100644 --- a/app/models/scoring-event.ts +++ b/app/models/scoring-event.ts @@ -425,6 +425,7 @@ export async function getUpcomingEventsForDraftedParticipants( eventDate: schema.scoringEvents.eventDate, eventType: schema.scoringEvents.eventType, sportsSeasonId: schema.scoringEvents.sportsSeasonId, + playoffMatchId: schema.playoffMatches.id, participant1Id: schema.playoffMatches.participant1Id, participant2Id: schema.playoffMatches.participant2Id, round: schema.playoffMatches.round, @@ -512,7 +513,9 @@ export async function getUpcomingEventsForDraftedParticipants( for (const row of rows) { const isSeries = (maxGameNumberByEvent.get(row.id) ?? 1) > 1; const entryKey = - isSeries && row.gameNumber !== null ? `${row.id}|${row.gameNumber}` : row.id; + isSeries && row.gameNumber !== null + ? `${row.id}|${row.playoffMatchId}|${row.gameNumber}` + : `${row.id}|${row.playoffMatchId}`; if (!entryMap.has(entryKey)) { entryMap.set(entryKey, {