From 1b51cb554bfaad442ff32efa304266f65faf6fb5 Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Fri, 22 May 2026 20:38:35 -0700 Subject: [PATCH] Fix Discord double-ping when autodraft fires immediately after manual pick When a manual pick triggered an immediate autodraft chain, both Discord announcements re-read currentPickNumber from the DB, which had already advanced past the chained pick. Both messages showed the same "On the clock" person, pinging them twice. Fix: pass nextPickNumber (the pick immediately following each specific pick, before any chain) as an explicit param to notifyPickMadeOnDiscord instead of re-reading from the DB. Also removes a now-unnecessary DB round-trip on every pick announcement. Co-Authored-By: Claude Sonnet 4.6 --- app/models/draft-utils.ts | 1 + app/routes/api/draft.make-pick.ts | 1 + .../__tests__/draft-discord.server.test.ts | 56 +++++++++++++------ app/services/draft-discord.server.ts | 39 ++++++------- 4 files changed, 58 insertions(+), 39 deletions(-) diff --git a/app/models/draft-utils.ts b/app/models/draft-utils.ts index 7479e31..91cdb6b 100644 --- a/app/models/draft-utils.ts +++ b/app/models/draft-utils.ts @@ -853,6 +853,7 @@ export async function executeAutoPick(params: { participantName: participantToPick.name, sportName: participantToPick.sportsSeason.sport.name, pickNumber, + nextPickNumber, round: currentRound, rawPickInRound, isDraftComplete, diff --git a/app/routes/api/draft.make-pick.ts b/app/routes/api/draft.make-pick.ts index d7446ec..ed89589 100644 --- a/app/routes/api/draft.make-pick.ts +++ b/app/routes/api/draft.make-pick.ts @@ -338,6 +338,7 @@ export async function action(args: ActionFunctionArgs) { participantName: participant.name, sportName: participant.sportsSeason.sport.name, pickNumber: currentPickNumber, + nextPickNumber, round: currentRound, rawPickInRound, isDraftComplete, diff --git a/app/services/__tests__/draft-discord.server.test.ts b/app/services/__tests__/draft-discord.server.test.ts index b0e3a90..ac5c9fb 100644 --- a/app/services/__tests__/draft-discord.server.test.ts +++ b/app/services/__tests__/draft-discord.server.test.ts @@ -40,6 +40,7 @@ const BASE_PARAMS = { participantName: "Erling Haaland", sportName: "Soccer", pickNumber: 1, + nextPickNumber: 2, round: 1, rawPickInRound: 1, isDraftComplete: false, @@ -56,10 +57,6 @@ function makeLeague(overrides: object = {}) { }; } -function makeSeason(currentPickNumber = 2) { - return { id: SEASON_ID, leagueId: LEAGUE_ID, currentPickNumber }; -} - function makeTeam(id: string, ownerId: string | null = null, name = "Beta United") { return { id, name, ownerId }; } @@ -70,19 +67,16 @@ function makeOwner(id: string, discordPingEnabled = true) { function makeMockDb(overrides: { league?: object | null; - season?: object | null; nextTeam?: object | null; owner?: object | null; } = {}) { const league = "league" in overrides ? overrides.league : makeLeague(); - const season = "season" in overrides ? overrides.season : makeSeason(); const nextTeam = "nextTeam" in overrides ? overrides.nextTeam : makeTeam(NEXT_TEAM_ID, NEXT_OWNER_ID); const owner = "owner" in overrides ? overrides.owner : makeOwner(NEXT_OWNER_ID); return { query: { leagues: { findFirst: vi.fn().mockResolvedValue(league) }, - seasons: { findFirst: vi.fn().mockResolvedValue(season) }, teams: { findFirst: vi.fn().mockResolvedValue(nextTeam) }, users: { findFirst: vi.fn().mockResolvedValue(owner) }, }, @@ -159,24 +153,52 @@ describe("notifyPickMadeOnDiscord", () => { ); }); - it("passes nextTeamName when fresh season resolves the next slot", async () => { - const db = makeMockDb({ season: makeSeason(2) }); // currentPickNumber=2 → pickInRound=2 → NEXT_TEAM_ID + it("passes nextTeamName based on caller-supplied nextPickNumber", async () => { + // pick 3 in a 2-team snake: round 2 (reversed) → draftOrder 2 → NEXT_TEAM_ID → "Beta United" + const db = makeMockDb(); - await notifyPickMadeOnDiscord({ ...BASE_PARAMS, db: db as never }); + await notifyPickMadeOnDiscord({ ...BASE_PARAMS, nextPickNumber: 3, db: db as never }); expect(sendPickAnnouncementNotification).toHaveBeenCalledWith( expect.objectContaining({ nextTeamName: "Beta United" }) ); }); - it("reads next team from fresh DB pick number, not caller-supplied snapshot", async () => { - // Simulate autodraft chain advancing 2 picks: currentPickNumber=3 on a 2-team snake - // draft means pickInRound=2 again (round 2 reversed) → NEXT_TEAM_ID - const db = makeMockDb({ season: makeSeason(3) }); + it("uses caller-supplied nextPickNumber instead of reading from DB", async () => { + const db = makeMockDb(); await notifyPickMadeOnDiscord({ ...BASE_PARAMS, db: db as never }); - expect(db.query.seasons.findFirst).toHaveBeenCalledTimes(1); + expect((db.query as Record).seasons).toBeUndefined(); + }); + + it("shows the immediate next drafter, not the post-autodraft-chain drafter", async () => { + // Regression: manual pick #1 by alpha; beta immediately autodrafts (#2); DB advances to pick #3. + // Discord for pick #1 must show beta ("On the clock: beta"), not gamma. + // Uses a 3-team snake: pick 1→alpha, pick 2→beta, pick 3→gamma. + const ALPHA_ID = "team-alpha"; + const BETA_ID = "team-beta"; + const GAMMA_ID = "team-gamma"; + const threeTeamSlots = [ + { teamId: ALPHA_ID, draftOrder: 1 }, + { teamId: BETA_ID, draftOrder: 2 }, + { teamId: GAMMA_ID, draftOrder: 3 }, + ]; + const betaTeam = { id: BETA_ID, name: "Beta Squad", ownerId: null }; + const db = makeMockDb({ nextTeam: betaTeam }); + + await notifyPickMadeOnDiscord({ + ...BASE_PARAMS, + pickNumber: 1, + nextPickNumber: 2, // immediately after alpha's pick — before beta's autodraft + totalTeams: 3, + draftSlots: threeTeamSlots, + db: db as never, + }); + + expect(sendPickAnnouncementNotification).toHaveBeenCalledWith( + expect.objectContaining({ nextTeamName: "Beta Squad" }) + ); }); it("passes nextOwnerDiscordId when owner has discordPingEnabled", async () => { @@ -216,7 +238,7 @@ describe("notifyPickMadeOnDiscord", () => { await notifyPickMadeOnDiscord({ ...BASE_PARAMS, isDraftComplete: true, db: db as never }); - expect(db.query.seasons.findFirst).not.toHaveBeenCalled(); + expect(db.query.teams.findFirst).not.toHaveBeenCalled(); expect(sendPickAnnouncementNotification).toHaveBeenCalledWith( expect.objectContaining({ isDraftComplete: true, nextTeamName: undefined }) ); @@ -235,7 +257,7 @@ describe("notifyPickMadeOnDiscord", () => { it("uses sequential (not snake-adjusted) pick number in Discord title for even rounds", async () => { // 13-team draft, pick #22: round 2 (even/reversed), rawPickInRound=9, snake-adjusted slot=5 // Discord should say "Round 2, Pick 9" not "Round 2, Pick 5" - const db = makeMockDb({ season: makeSeason(23) }); + const db = makeMockDb(); await notifyPickMadeOnDiscord({ ...BASE_PARAMS, diff --git a/app/services/draft-discord.server.ts b/app/services/draft-discord.server.ts index e46fe1b..5884e89 100644 --- a/app/services/draft-discord.server.ts +++ b/app/services/draft-discord.server.ts @@ -23,6 +23,7 @@ export async function notifyPickMadeOnDiscord(params: { participantName: string; sportName: string; pickNumber: number; + nextPickNumber: number; round: number; rawPickInRound: number; isDraftComplete: boolean; @@ -37,6 +38,7 @@ export async function notifyPickMadeOnDiscord(params: { participantName, sportName, pickNumber, + nextPickNumber, round, rawPickInRound, isDraftComplete, @@ -57,28 +59,21 @@ export async function notifyPickMadeOnDiscord(params: { let nextOwnerDiscordId: string | undefined; if (!isDraftComplete) { - // Read currentPickNumber fresh from DB so we see the post-autodraft-chain state, - // matching the same guarantee sendOnTheClockEmail provides. - const freshSeason = await db.query.seasons.findFirst({ - where: eq(schema.seasons.id, seasonId), - }); - if (freshSeason) { - const nextPickInRound = pickInRoundFor(freshSeason.currentPickNumber ?? 1, totalTeams); - const nextSlot = draftSlots.find((s) => s.draftOrder === nextPickInRound); - if (nextSlot) { - const nextTeam = await db.query.teams.findFirst({ - where: eq(schema.teams.id, nextSlot.teamId), - }); - if (nextTeam) { - nextTeamName = nextTeam.name; - if (nextTeam.ownerId) { - const owner = await db.query.users.findFirst({ - where: eq(schema.users.id, nextTeam.ownerId), - }); - if (owner?.discordPingEnabled) { - const discordIds = await findDiscordIdsByUserIds([owner.id]); - nextOwnerDiscordId = discordIds.get(owner.id); - } + const nextPickInRound = pickInRoundFor(nextPickNumber, totalTeams); + const nextSlot = draftSlots.find((s) => s.draftOrder === nextPickInRound); + if (nextSlot) { + const nextTeam = await db.query.teams.findFirst({ + where: eq(schema.teams.id, nextSlot.teamId), + }); + if (nextTeam) { + nextTeamName = nextTeam.name; + if (nextTeam.ownerId) { + const owner = await db.query.users.findFirst({ + where: eq(schema.users.id, nextTeam.ownerId), + }); + if (owner?.discordPingEnabled) { + const discordIds = await findDiscordIdsByUserIds([owner.id]); + nextOwnerDiscordId = discordIds.get(owner.id); } } } -- 2.45.3