100 lines
2.8 KiB
TypeScript
100 lines
2.8 KiB
TypeScript
|
|
import * as schema from "~/database/schema";
|
||
|
|
import { eq } from "drizzle-orm";
|
||
|
|
import { logger } from "~/lib/logger";
|
||
|
|
|
||
|
|
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
||
|
|
import type { Server as SocketIOServer } from "socket.io";
|
||
|
|
|
||
|
|
type Db = PostgresJsDatabase<typeof schema>;
|
||
|
|
|
||
|
|
export interface StartDraftOptions {
|
||
|
|
seasonId: string;
|
||
|
|
actorUserId: string;
|
||
|
|
actorDisplayName: string;
|
||
|
|
db: Db;
|
||
|
|
io: SocketIOServer;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface StartDraftResult {
|
||
|
|
success: boolean;
|
||
|
|
error?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Core draft-start logic shared by the commissioner HTTP route and the
|
||
|
|
* auto-start timer. Accepts `db` directly (not via AsyncLocalStorage) so it
|
||
|
|
* works both inside React Router request context and in the server timer loop.
|
||
|
|
*/
|
||
|
|
export async function startDraft(options: StartDraftOptions): Promise<StartDraftResult> {
|
||
|
|
const { seasonId, actorUserId, actorDisplayName, db, io } = options;
|
||
|
|
|
||
|
|
const season = await db.query.seasons.findFirst({
|
||
|
|
where: eq(schema.seasons.id, seasonId),
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!season) {
|
||
|
|
return { success: false, error: "Season not found" };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Idempotency guard — status may have changed between the outer check and here
|
||
|
|
if (season.status !== "pre_draft") {
|
||
|
|
return { success: false, error: "Draft already started or completed" };
|
||
|
|
}
|
||
|
|
|
||
|
|
const draftSlots = await db.query.draftSlots.findMany({
|
||
|
|
where: eq(schema.draftSlots.seasonId, seasonId),
|
||
|
|
});
|
||
|
|
|
||
|
|
if (draftSlots.length === 0) {
|
||
|
|
return { success: false, error: "No draft slots found for this season" };
|
||
|
|
}
|
||
|
|
|
||
|
|
await db
|
||
|
|
.update(schema.seasons)
|
||
|
|
.set({
|
||
|
|
status: "draft",
|
||
|
|
currentPickNumber: 1,
|
||
|
|
draftStartedAt: new Date(),
|
||
|
|
})
|
||
|
|
.where(eq(schema.seasons.id, seasonId));
|
||
|
|
|
||
|
|
// Standard mode: each pick gets exactly the increment time.
|
||
|
|
// Chess clock mode: each team starts with the full initial bank.
|
||
|
|
const initialTime =
|
||
|
|
season.draftTimerMode === "standard"
|
||
|
|
? season.draftIncrementTime || 30
|
||
|
|
: season.draftInitialTime || 120;
|
||
|
|
|
||
|
|
await db.delete(schema.draftTimers).where(eq(schema.draftTimers.seasonId, seasonId));
|
||
|
|
|
||
|
|
await db.insert(schema.draftTimers).values(
|
||
|
|
draftSlots.map((slot) => ({
|
||
|
|
seasonId,
|
||
|
|
teamId: slot.teamId,
|
||
|
|
timeRemaining: initialTime,
|
||
|
|
}))
|
||
|
|
);
|
||
|
|
|
||
|
|
await db.insert(schema.commissionerAuditLog).values({
|
||
|
|
seasonId,
|
||
|
|
leagueId: season.leagueId,
|
||
|
|
actorUserId,
|
||
|
|
actorDisplayName,
|
||
|
|
action: "draft_started",
|
||
|
|
affectedTeamIds: [],
|
||
|
|
details: { pickNumber: 1, autoStarted: actorUserId === "system" },
|
||
|
|
});
|
||
|
|
|
||
|
|
try {
|
||
|
|
io.to(`draft-${seasonId}`).emit("draft-started", {
|
||
|
|
seasonId,
|
||
|
|
currentPickNumber: 1,
|
||
|
|
});
|
||
|
|
} catch (err) {
|
||
|
|
// Socket emit failure is non-fatal; DB state is already committed
|
||
|
|
logger.error("[startDraft] Socket emit failed:", err);
|
||
|
|
}
|
||
|
|
|
||
|
|
return { success: true };
|
||
|
|
}
|