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.
This commit is contained in:
parent
13bc1e3b5f
commit
e03bdd538f
2 changed files with 68 additions and 8 deletions
|
|
@ -23,7 +23,7 @@ vi.mock("~/database/context", () => ({
|
||||||
|
|
||||||
vi.mock("~/database/schema", () => ({
|
vi.mock("~/database/schema", () => ({
|
||||||
scoringEvents: { sportsSeasonId: "se.sports_season_id", isComplete: "se.is_complete", eventDate: "se.event_date", eventType: "se.event_type" },
|
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" },
|
playoffMatchGames: { playoffMatchId: "pmg.playoff_match_id", scheduledAt: "pmg.scheduled_at", gameNumber: "pmg.game_number" },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -183,6 +183,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
const rows = [{
|
const rows = [{
|
||||||
id: "e1", name: "UCL QF", eventDate: "2025-04-09",
|
id: "e1", name: "UCL QF", eventDate: "2025-04-09",
|
||||||
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
||||||
|
playoffMatchId: "pm1",
|
||||||
participant1Id: "man-city", participant2Id: "bayern",
|
participant1Id: "man-city", participant2Id: "bayern",
|
||||||
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
||||||
}];
|
}];
|
||||||
|
|
@ -207,6 +208,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
{
|
{
|
||||||
id: "e1", name: "UCL QF", eventDate: "2025-04-09",
|
id: "e1", name: "UCL QF", eventDate: "2025-04-09",
|
||||||
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
||||||
|
playoffMatchId: "pm1",
|
||||||
participant1Id: "man-city", participant2Id: "arsenal",
|
participant1Id: "man-city", participant2Id: "arsenal",
|
||||||
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
||||||
},
|
},
|
||||||
|
|
@ -226,17 +228,19 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
expect(names).toContain("Arsenal");
|
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 = [
|
const rows = [
|
||||||
{
|
{
|
||||||
id: "e1", name: "UCL QF", eventDate: "2025-04-09",
|
id: "e1", name: "UCL QF", eventDate: "2025-04-09",
|
||||||
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
||||||
|
playoffMatchId: "pm1",
|
||||||
participant1Id: "man-city", participant2Id: "real-madrid",
|
participant1Id: "man-city", participant2Id: "real-madrid",
|
||||||
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "e1", name: "UCL QF", eventDate: "2025-04-09",
|
id: "e1", name: "UCL QF", eventDate: "2025-04-09",
|
||||||
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
||||||
|
playoffMatchId: "pm2",
|
||||||
participant1Id: "arsenal", participant2Id: "psg",
|
participant1Id: "arsenal", participant2Id: "psg",
|
||||||
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
||||||
},
|
},
|
||||||
|
|
@ -249,15 +253,21 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
DATE_FROM, DATE_TO
|
DATE_FROM, DATE_TO
|
||||||
);
|
);
|
||||||
|
|
||||||
// Both matches are in the same event → one result with both picks
|
// Each match is now a separate entry
|
||||||
expect(result).toHaveLength(1);
|
expect(result).toHaveLength(2);
|
||||||
expect(result[0].relevantParticipants).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 () => {
|
it("only includes drafted participants in relevantParticipants, not opponents", async () => {
|
||||||
const rows = [{
|
const rows = [{
|
||||||
id: "e1", name: "UCL QF", eventDate: "2025-04-09",
|
id: "e1", name: "UCL QF", eventDate: "2025-04-09",
|
||||||
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
||||||
|
playoffMatchId: "pm1",
|
||||||
participant1Id: "man-city", participant2Id: "real-madrid",
|
participant1Id: "man-city", participant2Id: "real-madrid",
|
||||||
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
round: "Quarterfinals", gameNumber: 1, scheduledAt: null,
|
||||||
}];
|
}];
|
||||||
|
|
@ -278,6 +288,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
mockDb.selectDistinct.mockReturnValueOnce(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,
|
||||||
|
playoffMatchId: "pm1",
|
||||||
participant1Id: "man-city", participant2Id: "arsenal",
|
participant1Id: "man-city", participant2Id: "arsenal",
|
||||||
round: "Semifinals", gameNumber: 1, scheduledAt: null,
|
round: "Semifinals", gameNumber: 1, scheduledAt: null,
|
||||||
}]));
|
}]));
|
||||||
|
|
@ -297,6 +308,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
id: "e1", name: "UCL QF",
|
id: "e1", name: "UCL QF",
|
||||||
eventDate: null, // ← no date on the event itself
|
eventDate: null, // ← no date on the event itself
|
||||||
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
||||||
|
playoffMatchId: "pm1",
|
||||||
participant1Id: "barcelona", participant2Id: "sporting",
|
participant1Id: "barcelona", participant2Id: "sporting",
|
||||||
round: "Knockout Stage", gameNumber: 1, scheduledAt: null,
|
round: "Knockout Stage", gameNumber: 1, scheduledAt: null,
|
||||||
}];
|
}];
|
||||||
|
|
@ -320,6 +332,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
id: "e1", name: "UCL QF",
|
id: "e1", name: "UCL QF",
|
||||||
eventDate: null,
|
eventDate: null,
|
||||||
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
||||||
|
playoffMatchId: "pm1",
|
||||||
participant1Id: "barcelona", participant2Id: "sporting",
|
participant1Id: "barcelona", participant2Id: "sporting",
|
||||||
round: "Knockout Stage", gameNumber: 1, scheduledAt: gameTime,
|
round: "Knockout Stage", gameNumber: 1, scheduledAt: gameTime,
|
||||||
}];
|
}];
|
||||||
|
|
@ -342,12 +355,14 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
{
|
{
|
||||||
id: "e1", name: "UCL QF", eventDate: null,
|
id: "e1", name: "UCL QF", eventDate: null,
|
||||||
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
||||||
|
playoffMatchId: "pm1",
|
||||||
participant1Id: "barcelona", participant2Id: "sporting",
|
participant1Id: "barcelona", participant2Id: "sporting",
|
||||||
round: "Knockout Stage", gameNumber: 1, scheduledAt: gameTime1,
|
round: "Knockout Stage", gameNumber: 1, scheduledAt: gameTime1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "e1", name: "UCL QF", eventDate: null,
|
id: "e1", name: "UCL QF", eventDate: null,
|
||||||
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
||||||
|
playoffMatchId: "pm1",
|
||||||
participant1Id: "barcelona", participant2Id: "sporting",
|
participant1Id: "barcelona", participant2Id: "sporting",
|
||||||
round: "Knockout Stage", gameNumber: 2, scheduledAt: gameTime2,
|
round: "Knockout Stage", gameNumber: 2, scheduledAt: gameTime2,
|
||||||
},
|
},
|
||||||
|
|
@ -366,8 +381,8 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result).toHaveLength(2);
|
expect(result).toHaveLength(2);
|
||||||
const game1 = result.find((r) => r.id === "e1|1");
|
const game1 = result.find((r) => r.id === "e1|pm1|1");
|
||||||
const game2 = result.find((r) => r.id === "e1|2");
|
const game2 = result.find((r) => r.id === "e1|pm1|2");
|
||||||
expect(game1?.earliestGameTime).toBe(gameTime1.toISOString());
|
expect(game1?.earliestGameTime).toBe(gameTime1.toISOString());
|
||||||
expect(game1?.matchLabel).toBe("Knockout Stage Game #1");
|
expect(game1?.matchLabel).toBe("Knockout Stage Game #1");
|
||||||
expect(game2?.earliestGameTime).toBe(gameTime2.toISOString());
|
expect(game2?.earliestGameTime).toBe(gameTime2.toISOString());
|
||||||
|
|
@ -378,6 +393,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
const rows = [{
|
const rows = [{
|
||||||
id: "e1", name: "PDC World Championship", eventDate: "2025-04-09",
|
id: "e1", name: "PDC World Championship", eventDate: "2025-04-09",
|
||||||
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
||||||
|
playoffMatchId: "pm1",
|
||||||
participant1Id: "man-city", participant2Id: "arsenal",
|
participant1Id: "man-city", participant2Id: "arsenal",
|
||||||
round: "Semifinals", gameNumber: 1, scheduledAt: null,
|
round: "Semifinals", gameNumber: 1, scheduledAt: null,
|
||||||
}];
|
}];
|
||||||
|
|
@ -398,6 +414,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
const rows = [{
|
const rows = [{
|
||||||
id: "e1", name: "Knockout Stage", eventDate: null,
|
id: "e1", name: "Knockout Stage", eventDate: null,
|
||||||
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
||||||
|
playoffMatchId: "pm1",
|
||||||
participant1Id: "barcelona", participant2Id: "sporting",
|
participant1Id: "barcelona", participant2Id: "sporting",
|
||||||
round: "Knockout Stage", gameNumber: 1, scheduledAt: null,
|
round: "Knockout Stage", gameNumber: 1, scheduledAt: null,
|
||||||
}];
|
}];
|
||||||
|
|
@ -416,6 +433,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
const rows = [{
|
const rows = [{
|
||||||
id: "e1", name: "PDC World Championship", eventDate: "2025-04-10",
|
id: "e1", name: "PDC World Championship", eventDate: "2025-04-10",
|
||||||
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
||||||
|
playoffMatchId: "pm1",
|
||||||
participant1Id: "man-city", participant2Id: "arsenal",
|
participant1Id: "man-city", participant2Id: "arsenal",
|
||||||
round: "Semifinals", gameNumber: 2, scheduledAt: null,
|
round: "Semifinals", gameNumber: 2, scheduledAt: null,
|
||||||
}];
|
}];
|
||||||
|
|
@ -441,6 +459,7 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
const mainRows = [{
|
const mainRows = [{
|
||||||
id: "e1", name: "NBA Finals", eventDate: "2025-04-10",
|
id: "e1", name: "NBA Finals", eventDate: "2025-04-10",
|
||||||
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
eventType: "playoff_game", sportsSeasonId: SPORTS_SEASON_ID,
|
||||||
|
playoffMatchId: "pm1",
|
||||||
participant1Id: "lakers", participant2Id: "celtics",
|
participant1Id: "lakers", participant2Id: "celtics",
|
||||||
round: "Finals", gameNumber: 1, scheduledAt: null,
|
round: "Finals", gameNumber: 1, scheduledAt: null,
|
||||||
}];
|
}];
|
||||||
|
|
@ -461,6 +480,44 @@ describe("getUpcomingEventsForDraftedParticipants — bracket sports", () => {
|
||||||
expect(result[0].matchLabel).toBe("Finals Game #1");
|
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 () => {
|
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([
|
||||||
|
|
|
||||||
|
|
@ -425,6 +425,7 @@ export async function getUpcomingEventsForDraftedParticipants(
|
||||||
eventDate: schema.scoringEvents.eventDate,
|
eventDate: schema.scoringEvents.eventDate,
|
||||||
eventType: schema.scoringEvents.eventType,
|
eventType: schema.scoringEvents.eventType,
|
||||||
sportsSeasonId: schema.scoringEvents.sportsSeasonId,
|
sportsSeasonId: schema.scoringEvents.sportsSeasonId,
|
||||||
|
playoffMatchId: schema.playoffMatches.id,
|
||||||
participant1Id: schema.playoffMatches.participant1Id,
|
participant1Id: schema.playoffMatches.participant1Id,
|
||||||
participant2Id: schema.playoffMatches.participant2Id,
|
participant2Id: schema.playoffMatches.participant2Id,
|
||||||
round: schema.playoffMatches.round,
|
round: schema.playoffMatches.round,
|
||||||
|
|
@ -512,7 +513,9 @@ export async function getUpcomingEventsForDraftedParticipants(
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const isSeries = (maxGameNumberByEvent.get(row.id) ?? 1) > 1;
|
const isSeries = (maxGameNumberByEvent.get(row.id) ?? 1) > 1;
|
||||||
const entryKey =
|
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)) {
|
if (!entryMap.has(entryKey)) {
|
||||||
entryMap.set(entryKey, {
|
entryMap.set(entryKey, {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue