2025-10-16 00:32:48 -07:00
|
|
|
import { database } from "~/database/context";
|
|
|
|
|
import * as schema from "~/database/schema";
|
2025-10-24 21:12:07 -07:00
|
|
|
import { eq, and, notInArray, desc, inArray } from "drizzle-orm";
|
2025-10-16 00:32:48 -07:00
|
|
|
import { getTeamQueue } from "./draft-queue";
|
2025-10-24 21:12:07 -07:00
|
|
|
import { isParticipantDrafted, getDraftPicksWithSports, getTeamDraftPicksWithSports } from "./draft-pick";
|
|
|
|
|
import { getParticipantsForSeasonWithSports } from "./participant";
|
|
|
|
|
import { getSeasonSportsSimple } from "./season-sport";
|
|
|
|
|
import { calculateDraftEligibility } from "~/lib/draft-eligibility";
|
2025-10-16 00:32:48 -07:00
|
|
|
|
2025-10-25 22:11:10 -07:00
|
|
|
/**
|
|
|
|
|
* Check if the next team has autodraft enabled and immediately execute their pick
|
|
|
|
|
* This is called after a pick is made to chain autodraft picks
|
|
|
|
|
*/
|
|
|
|
|
export async function checkAndTriggerNextAutodraft(params: {
|
|
|
|
|
seasonId: string;
|
|
|
|
|
nextPickNumber: number;
|
|
|
|
|
totalTeams: number;
|
|
|
|
|
draftSlots: any[];
|
|
|
|
|
db?: ReturnType<typeof database>;
|
|
|
|
|
}): Promise<void> {
|
|
|
|
|
const { seasonId, nextPickNumber, totalTeams, draftSlots, db: providedDb } = params;
|
|
|
|
|
const db = providedDb || database();
|
|
|
|
|
|
|
|
|
|
// Calculate which team is next using snake draft logic
|
|
|
|
|
const nextRound = Math.ceil(nextPickNumber / totalTeams);
|
|
|
|
|
const isNextRoundEven = nextRound % 2 === 0;
|
|
|
|
|
let nextPickInRound = ((nextPickNumber - 1) % totalTeams) + 1;
|
|
|
|
|
|
|
|
|
|
// Apply snake draft reversal for even rounds
|
|
|
|
|
if (isNextRoundEven) {
|
|
|
|
|
nextPickInRound = totalTeams - nextPickInRound + 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nextDraftSlot = draftSlots.find((slot) => slot.draftOrder === nextPickInRound);
|
|
|
|
|
if (!nextDraftSlot) return;
|
|
|
|
|
|
|
|
|
|
const nextTeamId = nextDraftSlot.teamId;
|
|
|
|
|
|
|
|
|
|
// Check if next team has autodraft enabled
|
|
|
|
|
const autodraftSettings = await db.query.autodraftSettings.findFirst({
|
|
|
|
|
where: and(
|
|
|
|
|
eq(schema.autodraftSettings.seasonId, seasonId),
|
|
|
|
|
eq(schema.autodraftSettings.teamId, nextTeamId)
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (autodraftSettings?.isEnabled) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[AutodraftChain] Team ${nextTeamId} has autodraft enabled, triggering immediate pick for pick ${nextPickNumber}`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Immediately execute autopick for this team
|
|
|
|
|
await executeAutoPick({
|
|
|
|
|
seasonId,
|
|
|
|
|
teamId: nextTeamId,
|
|
|
|
|
pickNumber: nextPickNumber,
|
|
|
|
|
triggeredBy: "timer", // Use "timer" to indicate automatic (not commissioner-forced)
|
|
|
|
|
autodraftSettings,
|
|
|
|
|
db,
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
console.log(
|
|
|
|
|
`[AutodraftChain] Team ${nextTeamId} does not have autodraft enabled, waiting for manual pick`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-16 00:32:48 -07:00
|
|
|
/**
|
|
|
|
|
* Auto-pick for a team when their timer runs out
|
2025-10-25 10:14:36 -07:00
|
|
|
* 1. Check queue - pick first eligible item if available (cleans up ineligible items)
|
2025-10-24 21:12:07 -07:00
|
|
|
* 2. If queue empty, pick highest EV participant not drafted from eligible sports
|
|
|
|
|
*
|
|
|
|
|
* Updated to respect Omni league draft eligibility rules
|
2025-10-16 00:32:48 -07:00
|
|
|
*/
|
2025-10-24 21:12:07 -07:00
|
|
|
export async function autoPickForTeam(
|
|
|
|
|
seasonId: string,
|
|
|
|
|
teamId: string,
|
|
|
|
|
draftRounds: number,
|
2025-10-26 20:35:55 -07:00
|
|
|
allTeamIds: string[],
|
|
|
|
|
providedDb?: ReturnType<typeof database>
|
2025-10-24 21:12:07 -07:00
|
|
|
) {
|
2025-10-26 20:35:55 -07:00
|
|
|
const db = providedDb || database();
|
2025-10-24 21:12:07 -07:00
|
|
|
|
|
|
|
|
// Calculate eligibility for this team
|
2025-10-26 20:35:55 -07:00
|
|
|
const allPicks = await getDraftPicksWithSports(seasonId, db);
|
|
|
|
|
const teamPicks = await getTeamDraftPicksWithSports(teamId, seasonId, db);
|
|
|
|
|
const allParticipants = await getParticipantsForSeasonWithSports(seasonId, db);
|
|
|
|
|
const seasonSports = await getSeasonSportsSimple(seasonId, db);
|
2025-10-24 21:12:07 -07:00
|
|
|
const allTeams = allTeamIds.map((id) => ({ id }));
|
|
|
|
|
|
|
|
|
|
const eligibility = calculateDraftEligibility(
|
|
|
|
|
teamId,
|
|
|
|
|
teamPicks,
|
|
|
|
|
allPicks,
|
|
|
|
|
allParticipants,
|
|
|
|
|
seasonSports,
|
|
|
|
|
draftRounds,
|
|
|
|
|
allTeams
|
|
|
|
|
);
|
|
|
|
|
|
2025-10-25 10:14:36 -07:00
|
|
|
console.log(
|
|
|
|
|
`[AutoPick] Team ${teamId} eligible sports:`,
|
|
|
|
|
Array.from(eligibility.eligibleSportIds)
|
|
|
|
|
);
|
|
|
|
|
|
2025-10-24 21:12:07 -07:00
|
|
|
// Check queue first - filter by eligible sports
|
2025-10-26 20:35:55 -07:00
|
|
|
const queue = await getTeamQueue(teamId, db);
|
2025-10-24 21:12:07 -07:00
|
|
|
|
2025-10-16 00:32:48 -07:00
|
|
|
if (queue.length > 0) {
|
2025-10-25 10:14:36 -07:00
|
|
|
console.log(`[AutoPick] Team ${teamId} has ${queue.length} items in queue`);
|
|
|
|
|
|
|
|
|
|
// Get participant details for queue items to check eligibility
|
2025-10-24 21:12:07 -07:00
|
|
|
const queueParticipantIds = queue.map((item) => item.participantId);
|
|
|
|
|
const queueParticipants = await db.query.participants.findMany({
|
|
|
|
|
where: inArray(schema.participants.id, queueParticipantIds),
|
|
|
|
|
with: {
|
|
|
|
|
sportsSeason: {
|
|
|
|
|
with: {
|
|
|
|
|
sport: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-25 10:14:36 -07:00
|
|
|
const ineligibleQueueItemIds: string[] = [];
|
|
|
|
|
|
|
|
|
|
// Try queue items in order, checking both drafted status and sport eligibility
|
2025-10-24 21:12:07 -07:00
|
|
|
for (const item of queue) {
|
|
|
|
|
const participant = queueParticipants.find((p) => p.id === item.participantId);
|
2025-10-25 10:14:36 -07:00
|
|
|
if (!participant) {
|
|
|
|
|
console.log(`[AutoPick] Queue item ${item.id} - participant not found, will remove`);
|
|
|
|
|
ineligibleQueueItemIds.push(item.id);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2025-10-24 21:12:07 -07:00
|
|
|
|
|
|
|
|
const sportId = participant.sportsSeason.sport.id;
|
|
|
|
|
const isEligible = eligibility.eligibleSportIds.has(sportId);
|
2025-10-16 00:32:48 -07:00
|
|
|
const isDrafted = await isParticipantDrafted(seasonId, item.participantId);
|
2025-10-24 21:12:07 -07:00
|
|
|
|
2025-10-25 10:14:36 -07:00
|
|
|
if (isDrafted) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[AutoPick] Queue item ${participant.name} (${participant.sportsSeason.sport.name}) - already drafted, will remove`
|
|
|
|
|
);
|
|
|
|
|
ineligibleQueueItemIds.push(item.id);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!isEligible) {
|
|
|
|
|
console.log(
|
|
|
|
|
`[AutoPick] Queue item ${participant.name} (${participant.sportsSeason.sport.name}) - not eligible for this team, will remove`
|
|
|
|
|
);
|
|
|
|
|
ineligibleQueueItemIds.push(item.id);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Found a valid pick from queue
|
|
|
|
|
console.log(
|
|
|
|
|
`[AutoPick] Selecting from queue: ${participant.name} (${participant.sportsSeason.sport.name})`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Clean up ineligible items from queue before returning
|
|
|
|
|
if (ineligibleQueueItemIds.length > 0) {
|
|
|
|
|
await db
|
|
|
|
|
.delete(schema.draftQueue)
|
|
|
|
|
.where(inArray(schema.draftQueue.id, ineligibleQueueItemIds));
|
|
|
|
|
console.log(`[AutoPick] Removed ${ineligibleQueueItemIds.length} ineligible items from queue`);
|
2025-10-16 00:32:48 -07:00
|
|
|
}
|
2025-10-25 10:14:36 -07:00
|
|
|
|
|
|
|
|
return item.participantId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// All queue items were ineligible or drafted - clean them up
|
|
|
|
|
if (ineligibleQueueItemIds.length > 0) {
|
|
|
|
|
await db
|
|
|
|
|
.delete(schema.draftQueue)
|
|
|
|
|
.where(inArray(schema.draftQueue.id, ineligibleQueueItemIds));
|
|
|
|
|
console.log(
|
|
|
|
|
`[AutoPick] Removed ${ineligibleQueueItemIds.length} ineligible items from queue (all items were invalid)`
|
|
|
|
|
);
|
2025-10-16 00:32:48 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-10-24 21:12:07 -07:00
|
|
|
|
|
|
|
|
// Queue is empty or all queued players drafted/ineligible
|
|
|
|
|
// Pick highest EV available from eligible sports
|
2025-10-25 10:14:36 -07:00
|
|
|
console.log(`[AutoPick] No valid queue items, selecting highest EV from eligible sports`);
|
2025-10-26 20:35:55 -07:00
|
|
|
return await getTopAvailableParticipant(seasonId, eligibility.eligibleSportIds, db);
|
2025-10-16 00:32:48 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the highest EV participant that hasn't been drafted yet
|
2025-10-24 21:12:07 -07:00
|
|
|
* Updated to filter by eligible sports
|
2025-10-16 00:32:48 -07:00
|
|
|
*/
|
2025-10-24 21:12:07 -07:00
|
|
|
export async function getTopAvailableParticipant(
|
|
|
|
|
seasonId: string,
|
2025-10-26 20:35:55 -07:00
|
|
|
eligibleSportIds?: Set<string>,
|
|
|
|
|
providedDb?: ReturnType<typeof database>
|
2025-10-24 21:12:07 -07:00
|
|
|
) {
|
2025-10-26 20:35:55 -07:00
|
|
|
const db = providedDb || database();
|
2025-10-16 00:32:48 -07:00
|
|
|
|
|
|
|
|
// Get all drafted participant IDs
|
|
|
|
|
const draftedPicks = await db
|
|
|
|
|
.select({ participantId: schema.draftPicks.participantId })
|
|
|
|
|
.from(schema.draftPicks)
|
|
|
|
|
.where(eq(schema.draftPicks.seasonId, seasonId));
|
|
|
|
|
|
|
|
|
|
const draftedIds = draftedPicks.map((p: { participantId: string }) => p.participantId);
|
|
|
|
|
|
2025-10-24 21:12:07 -07:00
|
|
|
// Get all participants from season sports, filtered by eligible sports if provided
|
|
|
|
|
let seasonSportsData;
|
|
|
|
|
if (eligibleSportIds && eligibleSportIds.size > 0) {
|
|
|
|
|
// Filter to only eligible sports
|
|
|
|
|
seasonSportsData = await db
|
|
|
|
|
.select({
|
|
|
|
|
sportsSeasonId: schema.seasonSports.sportsSeasonId,
|
|
|
|
|
sportId: schema.sports.id,
|
|
|
|
|
})
|
|
|
|
|
.from(schema.seasonSports)
|
|
|
|
|
.innerJoin(
|
|
|
|
|
schema.sportsSeasons,
|
|
|
|
|
eq(schema.seasonSports.sportsSeasonId, schema.sportsSeasons.id)
|
|
|
|
|
)
|
|
|
|
|
.innerJoin(
|
|
|
|
|
schema.sports,
|
|
|
|
|
eq(schema.sportsSeasons.sportId, schema.sports.id)
|
|
|
|
|
)
|
|
|
|
|
.where(eq(schema.seasonSports.seasonId, seasonId));
|
|
|
|
|
|
|
|
|
|
// Filter to only eligible sports
|
|
|
|
|
seasonSportsData = seasonSportsData.filter((s: { sportId: string }) =>
|
|
|
|
|
eligibleSportIds.has(s.sportId)
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
// No filtering - get all sports
|
|
|
|
|
seasonSportsData = await db
|
|
|
|
|
.select({ sportsSeasonId: schema.seasonSports.sportsSeasonId })
|
|
|
|
|
.from(schema.seasonSports)
|
|
|
|
|
.where(eq(schema.seasonSports.seasonId, seasonId));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sportsSeasonIds = seasonSportsData.map(
|
|
|
|
|
(s: { sportsSeasonId: string }) => s.sportsSeasonId
|
|
|
|
|
);
|
|
|
|
|
|
2025-10-16 00:32:48 -07:00
|
|
|
if (sportsSeasonIds.length === 0) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get top available participant by EV
|
|
|
|
|
let query = db
|
|
|
|
|
.select()
|
|
|
|
|
.from(schema.participants)
|
|
|
|
|
.where(eq(schema.participants.sportsSeasonId, sportsSeasonIds[0]))
|
|
|
|
|
.orderBy(desc(schema.participants.expectedValue), schema.participants.name);
|
|
|
|
|
|
|
|
|
|
// Filter out drafted participants if any exist
|
|
|
|
|
if (draftedIds.length > 0) {
|
|
|
|
|
query = db
|
|
|
|
|
.select()
|
|
|
|
|
.from(schema.participants)
|
|
|
|
|
.where(
|
|
|
|
|
and(
|
|
|
|
|
eq(schema.participants.sportsSeasonId, sportsSeasonIds[0]),
|
|
|
|
|
notInArray(schema.participants.id, draftedIds)
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
.orderBy(desc(schema.participants.expectedValue), schema.participants.name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle multiple sports seasons
|
|
|
|
|
if (sportsSeasonIds.length > 1) {
|
|
|
|
|
// For simplicity, we'll query all and sort in memory
|
|
|
|
|
// In production, might want to optimize this
|
|
|
|
|
const allParticipants = [];
|
|
|
|
|
|
|
|
|
|
for (const sportsSeasonId of sportsSeasonIds) {
|
|
|
|
|
let participantQuery = db
|
|
|
|
|
.select()
|
|
|
|
|
.from(schema.participants)
|
|
|
|
|
.where(eq(schema.participants.sportsSeasonId, sportsSeasonId));
|
|
|
|
|
|
|
|
|
|
if (draftedIds.length > 0) {
|
|
|
|
|
participantQuery = db
|
|
|
|
|
.select()
|
|
|
|
|
.from(schema.participants)
|
|
|
|
|
.where(
|
|
|
|
|
and(
|
|
|
|
|
eq(schema.participants.sportsSeasonId, sportsSeasonId),
|
|
|
|
|
notInArray(schema.participants.id, draftedIds)
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const seasonParticipants = await participantQuery;
|
|
|
|
|
allParticipants.push(...seasonParticipants);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sort by EV desc, then name
|
|
|
|
|
allParticipants.sort((a, b) => {
|
|
|
|
|
if (b.expectedValue !== a.expectedValue) {
|
|
|
|
|
return b.expectedValue - a.expectedValue;
|
|
|
|
|
}
|
|
|
|
|
return a.name.localeCompare(b.name);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return allParticipants[0]?.id || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [topParticipant] = await query;
|
|
|
|
|
return topParticipant?.id || null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Calculate the current pick based on draft order and round
|
|
|
|
|
*/
|
|
|
|
|
export function calculatePickInfo(
|
|
|
|
|
pickNumber: number,
|
|
|
|
|
teamCount: number
|
|
|
|
|
): { round: number; pickInRound: number; teamIndex: number } {
|
|
|
|
|
const round = Math.ceil(pickNumber / teamCount);
|
|
|
|
|
const pickInRound = ((pickNumber - 1) % teamCount) + 1;
|
|
|
|
|
|
|
|
|
|
// Snake draft: odd rounds go forward, even rounds go backward
|
|
|
|
|
const isOddRound = round % 2 === 1;
|
|
|
|
|
const teamIndex = isOddRound ? pickInRound - 1 : teamCount - pickInRound;
|
|
|
|
|
|
|
|
|
|
return { round, pickInRound, teamIndex };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the team ID for a given pick number based on draft order
|
|
|
|
|
*/
|
|
|
|
|
export function getTeamForPick(
|
|
|
|
|
pickNumber: number,
|
|
|
|
|
draftOrder: { teamId: string; draftOrder: number }[]
|
|
|
|
|
): string | null {
|
|
|
|
|
const sortedOrder = [...draftOrder].sort((a, b) => a.draftOrder - b.draftOrder);
|
|
|
|
|
const teamCount = sortedOrder.length;
|
2025-10-25 10:14:36 -07:00
|
|
|
|
2025-10-16 00:32:48 -07:00
|
|
|
if (teamCount === 0) return null;
|
2025-10-25 10:14:36 -07:00
|
|
|
|
2025-10-16 00:32:48 -07:00
|
|
|
const { teamIndex } = calculatePickInfo(pickNumber, teamCount);
|
|
|
|
|
return sortedOrder[teamIndex]?.teamId || null;
|
|
|
|
|
}
|
2025-10-25 10:14:36 -07:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Execute an autopick for a team - unified function for both commissioner-forced and timer-based autopicks
|
|
|
|
|
*
|
|
|
|
|
* Selection Logic:
|
|
|
|
|
* 1. Uses the team's draft queue first (prioritizes manager's preferences)
|
|
|
|
|
* 2. Validates each queued participant for:
|
|
|
|
|
* - Not already drafted
|
|
|
|
|
* - Eligible based on draft rules (sport eligibility, flex spots, etc.)
|
|
|
|
|
* 3. Automatically removes ineligible participants from queue
|
|
|
|
|
* 4. If queue is empty or all items are ineligible, selects highest EV participant from eligible sports
|
|
|
|
|
*
|
|
|
|
|
* This ensures autopicks ALWAYS respect draft eligibility rules and cannot make illegal selections.
|
|
|
|
|
*
|
|
|
|
|
* @param params.seasonId - The season ID
|
|
|
|
|
* @param params.teamId - The team making the pick
|
|
|
|
|
* @param params.pickNumber - The current pick number
|
|
|
|
|
* @param params.triggeredBy - Who/what triggered the autopick ("commissioner" or "timer")
|
|
|
|
|
* @param params.commissionerUserId - User ID of commissioner (required if triggeredBy is "commissioner")
|
|
|
|
|
* @param params.autodraftSettings - Autodraft settings (used for timer-based picks)
|
|
|
|
|
* @param params.db - Database instance (optional, will use database() if not provided)
|
|
|
|
|
* @returns Result object with success status and pick data
|
|
|
|
|
*/
|
|
|
|
|
export async function executeAutoPick(params: {
|
|
|
|
|
seasonId: string;
|
|
|
|
|
teamId: string;
|
|
|
|
|
pickNumber: number;
|
|
|
|
|
triggeredBy: "commissioner" | "timer";
|
|
|
|
|
commissionerUserId?: string;
|
|
|
|
|
autodraftSettings?: any;
|
|
|
|
|
db?: ReturnType<typeof database>;
|
|
|
|
|
}): Promise<{
|
|
|
|
|
success: boolean;
|
|
|
|
|
error?: string;
|
|
|
|
|
pick?: any;
|
|
|
|
|
participant?: any;
|
|
|
|
|
nextPickNumber?: number;
|
|
|
|
|
isDraftComplete?: boolean;
|
|
|
|
|
}> {
|
|
|
|
|
const {
|
|
|
|
|
seasonId,
|
|
|
|
|
teamId,
|
|
|
|
|
pickNumber,
|
|
|
|
|
triggeredBy,
|
|
|
|
|
commissionerUserId,
|
|
|
|
|
autodraftSettings,
|
|
|
|
|
db: providedDb,
|
|
|
|
|
} = params;
|
|
|
|
|
|
|
|
|
|
const db = providedDb || database();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Race condition protection - check if pick already made
|
|
|
|
|
const existingPick = await db.query.draftPicks.findFirst({
|
|
|
|
|
where: and(
|
|
|
|
|
eq(schema.draftPicks.seasonId, seasonId),
|
|
|
|
|
eq(schema.draftPicks.pickNumber, pickNumber)
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (existingPick) {
|
|
|
|
|
console.log(`[AutoPick] Pick ${pickNumber} already made, skipping`);
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
error: "Pick already made",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get season details
|
|
|
|
|
const season = await db.query.seasons.findFirst({
|
|
|
|
|
where: eq(schema.seasons.id, seasonId),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!season) {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
error: "Season not found",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get draft slots to calculate round/pickInRound and get all team IDs
|
|
|
|
|
const draftSlots = await db.query.draftSlots.findMany({
|
|
|
|
|
where: eq(schema.draftSlots.seasonId, seasonId),
|
|
|
|
|
orderBy: schema.draftSlots.draftOrder,
|
|
|
|
|
with: {
|
|
|
|
|
team: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const totalTeams = draftSlots.length;
|
|
|
|
|
if (totalTeams === 0) {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
error: "No draft slots found",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const allTeamIds = draftSlots.map((slot) => slot.teamId);
|
|
|
|
|
|
|
|
|
|
// Use autoPickForTeam to select participant (respects eligibility and queue)
|
|
|
|
|
const participantId = await autoPickForTeam(
|
|
|
|
|
seasonId,
|
|
|
|
|
teamId,
|
|
|
|
|
season.draftRounds,
|
2025-10-26 20:35:55 -07:00
|
|
|
allTeamIds,
|
|
|
|
|
db
|
2025-10-25 10:14:36 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!participantId) {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
error: "No eligible participants available to pick",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get participant details
|
|
|
|
|
const participantToPick = await db.query.participants.findFirst({
|
|
|
|
|
where: eq(schema.participants.id, participantId),
|
|
|
|
|
with: {
|
|
|
|
|
sportsSeason: {
|
|
|
|
|
with: {
|
|
|
|
|
sport: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!participantToPick) {
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
error: "Participant not found",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Calculate round and pickInRound with snake draft logic
|
|
|
|
|
const currentRound = Math.ceil(pickNumber / totalTeams);
|
|
|
|
|
const isEvenRound = currentRound % 2 === 0;
|
|
|
|
|
let pickInRound = ((pickNumber - 1) % totalTeams) + 1;
|
|
|
|
|
|
|
|
|
|
// Apply snake draft reversal for even rounds
|
|
|
|
|
if (isEvenRound) {
|
|
|
|
|
pickInRound = totalTeams - pickInRound + 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Determine pickedByUserId based on trigger
|
|
|
|
|
const pickedByUserId = triggeredBy === "commissioner"
|
|
|
|
|
? (commissionerUserId || "")
|
|
|
|
|
: "";
|
|
|
|
|
|
|
|
|
|
// Create the draft pick
|
|
|
|
|
const [draftPick] = await db
|
|
|
|
|
.insert(schema.draftPicks)
|
|
|
|
|
.values({
|
|
|
|
|
seasonId,
|
|
|
|
|
teamId,
|
|
|
|
|
participantId: participantToPick.id,
|
|
|
|
|
pickNumber,
|
|
|
|
|
round: currentRound,
|
|
|
|
|
pickInRound,
|
|
|
|
|
pickedByUserId,
|
|
|
|
|
pickedByType: "auto",
|
|
|
|
|
timeUsed: triggeredBy === "timer" ? (season.draftInitialTime || 120) : undefined,
|
|
|
|
|
})
|
|
|
|
|
.returning();
|
|
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
|
`[AutoPick] Pick created - ${triggeredBy} triggered - Pick ${pickNumber} - Participant ${participantId}`
|
|
|
|
|
);
|
|
|
|
|
|
2025-10-25 22:11:10 -07:00
|
|
|
// Calculate next pick info (before updating season)
|
2025-10-25 10:14:36 -07:00
|
|
|
const nextPickNumber = pickNumber + 1;
|
|
|
|
|
const totalPicks = totalTeams * season.draftRounds;
|
|
|
|
|
const isDraftComplete = nextPickNumber > totalPicks;
|
|
|
|
|
|
|
|
|
|
// Add increment to the team that just picked
|
|
|
|
|
const currentTimer = await db.query.draftTimers.findFirst({
|
|
|
|
|
where: and(
|
|
|
|
|
eq(schema.draftTimers.seasonId, seasonId),
|
|
|
|
|
eq(schema.draftTimers.teamId, teamId)
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (currentTimer) {
|
|
|
|
|
const incrementTime = season.draftIncrementTime || 30;
|
|
|
|
|
const newTimeRemaining = currentTimer.timeRemaining + incrementTime;
|
|
|
|
|
await db
|
|
|
|
|
.update(schema.draftTimers)
|
|
|
|
|
.set({
|
|
|
|
|
timeRemaining: newTimeRemaining,
|
|
|
|
|
updatedAt: new Date(),
|
|
|
|
|
})
|
|
|
|
|
.where(eq(schema.draftTimers.id, currentTimer.id));
|
|
|
|
|
|
|
|
|
|
// Emit timer update to all clients
|
|
|
|
|
try {
|
|
|
|
|
const io = (global as any).__socketIO;
|
|
|
|
|
if (io) {
|
|
|
|
|
io.to(`draft-${seasonId}`).emit("timer-update", {
|
|
|
|
|
seasonId,
|
|
|
|
|
teamId,
|
|
|
|
|
timeRemaining: newTimeRemaining,
|
|
|
|
|
currentPickNumber: pickNumber,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("[AutoPick] Socket.IO timer-update error:", error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-25 22:11:10 -07:00
|
|
|
// Initialize timer for the next team BEFORE updating pick number (prevents race condition)
|
|
|
|
|
if (!isDraftComplete) {
|
|
|
|
|
// Calculate which team is next using snake draft logic
|
|
|
|
|
const nextRound = Math.ceil(nextPickNumber / totalTeams);
|
|
|
|
|
const isNextRoundEven = nextRound % 2 === 0;
|
|
|
|
|
let nextPickInRound = ((nextPickNumber - 1) % totalTeams) + 1;
|
|
|
|
|
|
|
|
|
|
// Apply snake draft reversal for even rounds
|
|
|
|
|
if (isNextRoundEven) {
|
|
|
|
|
nextPickInRound = totalTeams - nextPickInRound + 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nextDraftSlot = draftSlots.find((slot) => slot.draftOrder === nextPickInRound);
|
|
|
|
|
|
|
|
|
|
if (nextDraftSlot) {
|
|
|
|
|
const nextTeamId = nextDraftSlot.teamId;
|
|
|
|
|
const initialTime = season.draftInitialTime || 120;
|
|
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
|
`[AutoPick] Initializing timer for next team ${nextTeamId} with ${initialTime}s (pick ${nextPickNumber})`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Check if timer already exists for next team
|
|
|
|
|
const nextTimer = await db.query.draftTimers.findFirst({
|
|
|
|
|
where: and(
|
|
|
|
|
eq(schema.draftTimers.seasonId, seasonId),
|
|
|
|
|
eq(schema.draftTimers.teamId, nextTeamId)
|
|
|
|
|
),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (nextTimer) {
|
|
|
|
|
// Update existing timer
|
|
|
|
|
await db
|
|
|
|
|
.update(schema.draftTimers)
|
|
|
|
|
.set({
|
|
|
|
|
timeRemaining: initialTime,
|
|
|
|
|
updatedAt: new Date(),
|
|
|
|
|
})
|
|
|
|
|
.where(eq(schema.draftTimers.id, nextTimer.id));
|
|
|
|
|
} else {
|
|
|
|
|
// Create new timer if it doesn't exist
|
|
|
|
|
await db.insert(schema.draftTimers).values({
|
|
|
|
|
seasonId,
|
|
|
|
|
teamId: nextTeamId,
|
|
|
|
|
timeRemaining: initialTime,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Emit timer update for next team
|
|
|
|
|
try {
|
|
|
|
|
const io = (global as any).__socketIO;
|
|
|
|
|
if (io) {
|
|
|
|
|
io.to(`draft-${seasonId}`).emit("timer-update", {
|
|
|
|
|
seasonId,
|
|
|
|
|
teamId: nextTeamId,
|
|
|
|
|
timeRemaining: initialTime,
|
|
|
|
|
currentPickNumber: nextPickNumber,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("[AutoPick] Socket.IO next timer-update error:", error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update season's current pick number (AFTER initializing next timer to prevent race condition)
|
|
|
|
|
await db
|
|
|
|
|
.update(schema.seasons)
|
|
|
|
|
.set({
|
|
|
|
|
currentPickNumber: isDraftComplete ? pickNumber : nextPickNumber,
|
|
|
|
|
status: isDraftComplete ? "active" : season.status,
|
|
|
|
|
})
|
|
|
|
|
.where(eq(schema.seasons.id, seasonId));
|
|
|
|
|
|
2025-10-25 10:14:36 -07:00
|
|
|
// Remove from ALL team queues in this season (participant is now drafted)
|
|
|
|
|
await db
|
|
|
|
|
.delete(schema.draftQueue)
|
|
|
|
|
.where(
|
|
|
|
|
and(
|
|
|
|
|
eq(schema.draftQueue.seasonId, seasonId),
|
|
|
|
|
eq(schema.draftQueue.participantId, participantToPick.id)
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Handle autodraft settings for timer-based picks with "next_pick" mode
|
|
|
|
|
if (triggeredBy === "timer" && autodraftSettings?.isEnabled && autodraftSettings.mode === "next_pick") {
|
|
|
|
|
console.log(`[AutoPick] Disabling autodraft for team ${teamId} after next_pick`);
|
|
|
|
|
await db
|
|
|
|
|
.update(schema.autodraftSettings)
|
|
|
|
|
.set({
|
|
|
|
|
isEnabled: false,
|
|
|
|
|
updatedAt: new Date(),
|
|
|
|
|
})
|
|
|
|
|
.where(eq(schema.autodraftSettings.id, autodraftSettings.id));
|
|
|
|
|
|
|
|
|
|
// Emit autodraft-updated event
|
|
|
|
|
try {
|
|
|
|
|
const io = (global as any).__socketIO;
|
|
|
|
|
if (io) {
|
|
|
|
|
io.to(`draft-${seasonId}`).emit("autodraft-updated", {
|
|
|
|
|
teamId,
|
|
|
|
|
isEnabled: false,
|
|
|
|
|
mode: autodraftSettings.mode,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("[AutoPick] Socket.IO autodraft-updated error:", error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Emit socket events
|
|
|
|
|
try {
|
|
|
|
|
const io = (global as any).__socketIO;
|
|
|
|
|
if (io) {
|
|
|
|
|
const team = draftSlots.find((slot) => slot.team.id === teamId)?.team;
|
|
|
|
|
|
|
|
|
|
// Emit participant-removed-from-queues event
|
|
|
|
|
io.to(`draft-${seasonId}`).emit("participant-removed-from-queues", {
|
|
|
|
|
participantId: participantToPick.id,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Emit pick-made event
|
|
|
|
|
io.to(`draft-${seasonId}`).emit("pick-made", {
|
|
|
|
|
pick: {
|
|
|
|
|
...draftPick,
|
|
|
|
|
team,
|
|
|
|
|
participant: {
|
|
|
|
|
...participantToPick,
|
|
|
|
|
sport: participantToPick.sportsSeason.sport,
|
|
|
|
|
},
|
|
|
|
|
sport: participantToPick.sportsSeason.sport,
|
|
|
|
|
},
|
|
|
|
|
nextPickNumber: isDraftComplete ? pickNumber : nextPickNumber,
|
|
|
|
|
isDraftComplete,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Emit draft-completed event if applicable
|
|
|
|
|
if (isDraftComplete) {
|
|
|
|
|
io.to(`draft-${seasonId}`).emit("draft-completed");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("[AutoPick] Socket.IO events error:", error);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-25 22:11:10 -07:00
|
|
|
// Check if next team has autodraft enabled and trigger immediately
|
|
|
|
|
if (!isDraftComplete) {
|
|
|
|
|
await checkAndTriggerNextAutodraft({
|
|
|
|
|
seasonId,
|
|
|
|
|
nextPickNumber,
|
|
|
|
|
totalTeams,
|
|
|
|
|
draftSlots,
|
|
|
|
|
db,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-25 10:14:36 -07:00
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
pick: draftPick,
|
|
|
|
|
participant: participantToPick,
|
|
|
|
|
nextPickNumber: isDraftComplete ? pickNumber : nextPickNumber,
|
|
|
|
|
isDraftComplete,
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("[AutoPick] Error in executeAutoPick:", error);
|
|
|
|
|
return {
|
|
|
|
|
success: false,
|
|
|
|
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|