Fix Discord double-ping when autodraft fires after manual pick #47

Merged
chrisp merged 1 commit from fix/discord-autodraft-double-ping into main 2026-05-23 03:46:49 +00:00
4 changed files with 58 additions and 39 deletions

View file

@ -853,6 +853,7 @@ export async function executeAutoPick(params: {
participantName: participantToPick.name, participantName: participantToPick.name,
sportName: participantToPick.sportsSeason.sport.name, sportName: participantToPick.sportsSeason.sport.name,
pickNumber, pickNumber,
nextPickNumber,
round: currentRound, round: currentRound,
rawPickInRound, rawPickInRound,
isDraftComplete, isDraftComplete,

View file

@ -338,6 +338,7 @@ export async function action(args: ActionFunctionArgs) {
participantName: participant.name, participantName: participant.name,
sportName: participant.sportsSeason.sport.name, sportName: participant.sportsSeason.sport.name,
pickNumber: currentPickNumber, pickNumber: currentPickNumber,
nextPickNumber,
round: currentRound, round: currentRound,
rawPickInRound, rawPickInRound,
isDraftComplete, isDraftComplete,

View file

@ -40,6 +40,7 @@ const BASE_PARAMS = {
participantName: "Erling Haaland", participantName: "Erling Haaland",
sportName: "Soccer", sportName: "Soccer",
pickNumber: 1, pickNumber: 1,
nextPickNumber: 2,
round: 1, round: 1,
rawPickInRound: 1, rawPickInRound: 1,
isDraftComplete: false, 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") { function makeTeam(id: string, ownerId: string | null = null, name = "Beta United") {
return { id, name, ownerId }; return { id, name, ownerId };
} }
@ -70,19 +67,16 @@ function makeOwner(id: string, discordPingEnabled = true) {
function makeMockDb(overrides: { function makeMockDb(overrides: {
league?: object | null; league?: object | null;
season?: object | null;
nextTeam?: object | null; nextTeam?: object | null;
owner?: object | null; owner?: object | null;
} = {}) { } = {}) {
const league = "league" in overrides ? overrides.league : makeLeague(); 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 nextTeam = "nextTeam" in overrides ? overrides.nextTeam : makeTeam(NEXT_TEAM_ID, NEXT_OWNER_ID);
const owner = "owner" in overrides ? overrides.owner : makeOwner(NEXT_OWNER_ID); const owner = "owner" in overrides ? overrides.owner : makeOwner(NEXT_OWNER_ID);
return { return {
query: { query: {
leagues: { findFirst: vi.fn().mockResolvedValue(league) }, leagues: { findFirst: vi.fn().mockResolvedValue(league) },
seasons: { findFirst: vi.fn().mockResolvedValue(season) },
teams: { findFirst: vi.fn().mockResolvedValue(nextTeam) }, teams: { findFirst: vi.fn().mockResolvedValue(nextTeam) },
users: { findFirst: vi.fn().mockResolvedValue(owner) }, users: { findFirst: vi.fn().mockResolvedValue(owner) },
}, },
@ -159,24 +153,52 @@ describe("notifyPickMadeOnDiscord", () => {
); );
}); });
it("passes nextTeamName when fresh season resolves the next slot", async () => { it("passes nextTeamName based on caller-supplied nextPickNumber", async () => {
const db = makeMockDb({ season: makeSeason(2) }); // currentPickNumber=2 → pickInRound=2 → NEXT_TEAM_ID // 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(sendPickAnnouncementNotification).toHaveBeenCalledWith(
expect.objectContaining({ nextTeamName: "Beta United" }) expect.objectContaining({ nextTeamName: "Beta United" })
); );
}); });
it("reads next team from fresh DB pick number, not caller-supplied snapshot", async () => { it("uses caller-supplied nextPickNumber instead of reading from DB", async () => {
// Simulate autodraft chain advancing 2 picks: currentPickNumber=3 on a 2-team snake const db = makeMockDb();
// draft means pickInRound=2 again (round 2 reversed) → NEXT_TEAM_ID
const db = makeMockDb({ season: makeSeason(3) });
await notifyPickMadeOnDiscord({ ...BASE_PARAMS, db: db as never }); await notifyPickMadeOnDiscord({ ...BASE_PARAMS, db: db as never });
expect(db.query.seasons.findFirst).toHaveBeenCalledTimes(1); expect((db.query as Record<string, unknown>).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 () => { it("passes nextOwnerDiscordId when owner has discordPingEnabled", async () => {
@ -216,7 +238,7 @@ describe("notifyPickMadeOnDiscord", () => {
await notifyPickMadeOnDiscord({ ...BASE_PARAMS, isDraftComplete: true, db: db as never }); 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(sendPickAnnouncementNotification).toHaveBeenCalledWith(
expect.objectContaining({ isDraftComplete: true, nextTeamName: undefined }) 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 () => { 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 // 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" // Discord should say "Round 2, Pick 9" not "Round 2, Pick 5"
const db = makeMockDb({ season: makeSeason(23) }); const db = makeMockDb();
await notifyPickMadeOnDiscord({ await notifyPickMadeOnDiscord({
...BASE_PARAMS, ...BASE_PARAMS,

View file

@ -23,6 +23,7 @@ export async function notifyPickMadeOnDiscord(params: {
participantName: string; participantName: string;
sportName: string; sportName: string;
pickNumber: number; pickNumber: number;
nextPickNumber: number;
round: number; round: number;
rawPickInRound: number; rawPickInRound: number;
isDraftComplete: boolean; isDraftComplete: boolean;
@ -37,6 +38,7 @@ export async function notifyPickMadeOnDiscord(params: {
participantName, participantName,
sportName, sportName,
pickNumber, pickNumber,
nextPickNumber,
round, round,
rawPickInRound, rawPickInRound,
isDraftComplete, isDraftComplete,
@ -57,28 +59,21 @@ export async function notifyPickMadeOnDiscord(params: {
let nextOwnerDiscordId: string | undefined; let nextOwnerDiscordId: string | undefined;
if (!isDraftComplete) { if (!isDraftComplete) {
// Read currentPickNumber fresh from DB so we see the post-autodraft-chain state, const nextPickInRound = pickInRoundFor(nextPickNumber, totalTeams);
// matching the same guarantee sendOnTheClockEmail provides. const nextSlot = draftSlots.find((s) => s.draftOrder === nextPickInRound);
const freshSeason = await db.query.seasons.findFirst({ if (nextSlot) {
where: eq(schema.seasons.id, seasonId), const nextTeam = await db.query.teams.findFirst({
}); where: eq(schema.teams.id, nextSlot.teamId),
if (freshSeason) { });
const nextPickInRound = pickInRoundFor(freshSeason.currentPickNumber ?? 1, totalTeams); if (nextTeam) {
const nextSlot = draftSlots.find((s) => s.draftOrder === nextPickInRound); nextTeamName = nextTeam.name;
if (nextSlot) { if (nextTeam.ownerId) {
const nextTeam = await db.query.teams.findFirst({ const owner = await db.query.users.findFirst({
where: eq(schema.teams.id, nextSlot.teamId), where: eq(schema.users.id, nextTeam.ownerId),
}); });
if (nextTeam) { if (owner?.discordPingEnabled) {
nextTeamName = nextTeam.name; const discordIds = await findDiscordIdsByUserIds([owner.id]);
if (nextTeam.ownerId) { nextOwnerDiscordId = discordIds.get(owner.id);
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);
}
} }
} }
} }