Introduces three new schema tables (simulator_profiles, sports_season_simulator_configs, season_participant_simulator_inputs), a central model layer (app/models/simulator.ts), and a single runner entry point so every simulator run follows the same prepare → simulate → persist → snapshot → recalculate flow. Key additions: - manifest.ts: per-simulator display names, default configs, required/ optional inputs, derivable-input declarations, and setup sections - input-policy.ts: resolves sourceElo from projectedWins, projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds; supports block / fallbackElo / averageKnown / worstKnownMinus strategies - runner.ts: single entry point for admin simulation runs; materialises derived inputs, normalises result columns, zeroes omitted participants, snapshots EVs, and recalculates linked fantasy standings - /admin/simulators: inventory page with per-season readiness and bulk run - /admin/sports-seasons/:id/simulator: per-season setup page with readiness summary, input-policy editor, raw JSON config override, and CSV bulk input - NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs, falling back to the hardcoded name-keyed maps while DB data is being populated - Clone flow copies simulator config by default; volatile inputs (odds, Elo) only copied when explicitly requested Code-review fixes included in this commit: - source field in compatibility bridge checked with !== null instead of !== undefined - sourceEloRequirementLabel no longer appends "configured fallback" when the participant is already excluded from all resolved sources - Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel - save-config preserves existing inputPolicy when the submitted JSON omits it - Input table truncation label added (Showing 20 of N) - CSV description notes values must not contain commas - N+1 comment added to listSportsSeasonSimulatorSummaries - assertRegistrySchemaDriftFree called in manifest tests - Runner test suite added covering happy path, already-running guard, readiness failure, empty results, and error recovery with status reset Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1700 lines
69 KiB
TypeScript
1700 lines
69 KiB
TypeScript
import { check, integer, pgTable, varchar, uuid, timestamp, pgEnum, boolean, text, date, decimal, uniqueIndex, index, jsonb } from "drizzle-orm/pg-core";
|
||
import { relations, sql } from "drizzle-orm";
|
||
|
||
export const users = pgTable("users", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
clerkId: varchar("clerk_id", { length: 255 }).unique(), // legacy: only populated for users migrated from Clerk; new users are null
|
||
email: varchar("email", { length: 255 }).notNull(),
|
||
emailVerified: boolean("email_verified").notNull().default(false),
|
||
username: varchar("username", { length: 255 }),
|
||
displayName: varchar("display_name", { length: 255 }), // mapped as BetterAuth's "name" field
|
||
imageUrl: varchar("image_url", { length: 512 }), // mapped as BetterAuth's "image" field
|
||
flagConfig: jsonb("flag_config").$type<{ pattern: string; colors: string[] }>(),
|
||
customAvatarUrl: varchar("custom_avatar_url", { length: 512 }),
|
||
avatarType: varchar("avatar_type", { length: 20 }).notNull().default("flag"),
|
||
isAdmin: boolean("is_admin").notNull().default(false),
|
||
timezone: varchar("timezone", { length: 50 }), // IANA timezone string, e.g. "America/Chicago"
|
||
lastDataRequestAt: timestamp("last_data_request_at"),
|
||
deletedAt: timestamp("deleted_at"),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
});
|
||
|
||
// BetterAuth session/account/verification tables
|
||
export const sessions = pgTable("sessions", {
|
||
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||
expiresAt: timestamp("expires_at").notNull(),
|
||
token: text("token").notNull().unique(),
|
||
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
ipAddress: text("ip_address"),
|
||
userAgent: text("user_agent"),
|
||
});
|
||
|
||
export const accounts = pgTable("accounts", {
|
||
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||
accountId: text("account_id").notNull(),
|
||
providerId: text("provider_id").notNull(),
|
||
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
||
accessToken: text("access_token"),
|
||
refreshToken: text("refresh_token"),
|
||
idToken: text("id_token"),
|
||
expiresAt: timestamp("expires_at"),
|
||
accessTokenExpiresAt: timestamp("access_token_expires_at"),
|
||
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
|
||
scope: text("scope"),
|
||
password: text("password"),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
});
|
||
|
||
export const verifications = pgTable("verifications", {
|
||
id: text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||
identifier: text("identifier").notNull(),
|
||
value: text("value").notNull(),
|
||
expiresAt: timestamp("expires_at").notNull(),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
});
|
||
|
||
// Fantasy League Tables
|
||
export const seasonStatusEnum = pgEnum("season_status", [
|
||
"pre_draft",
|
||
"draft",
|
||
"active",
|
||
"completed",
|
||
]);
|
||
|
||
// Sports Tables - Enums
|
||
export const sportTypeEnum = pgEnum("sport_type", ["team", "individual"]);
|
||
|
||
export const sportsSeasonStatusEnum = pgEnum("sports_season_status", [
|
||
"upcoming",
|
||
"active",
|
||
"completed",
|
||
]);
|
||
|
||
export const scoringTypeEnum = pgEnum("scoring_type", [
|
||
"playoffs",
|
||
"regular_season",
|
||
"majors",
|
||
]);
|
||
|
||
export const scoringPatternEnum = pgEnum("scoring_pattern", [
|
||
"playoff_bracket",
|
||
"season_standings",
|
||
"qualifying_points",
|
||
]);
|
||
|
||
export const eventTypeEnum = pgEnum("event_type", [
|
||
"playoff_game",
|
||
"major_tournament",
|
||
"final_standings",
|
||
"schedule_event",
|
||
]);
|
||
|
||
export const pickedByTypeEnum = pgEnum("picked_by_type", [
|
||
"owner",
|
||
"commissioner",
|
||
"admin",
|
||
"auto",
|
||
]);
|
||
|
||
export const autodraftModeEnum = pgEnum("autodraft_mode", [
|
||
"next_pick",
|
||
"while_on",
|
||
]);
|
||
|
||
export const draftTimerModeEnum = pgEnum("draft_timer_mode", [
|
||
"chess_clock",
|
||
"standard",
|
||
]);
|
||
|
||
export const overnightPauseModeEnum = pgEnum("overnight_pause_mode", [
|
||
"none",
|
||
"league",
|
||
"per_user",
|
||
]);
|
||
|
||
export const probabilitySourceEnum = pgEnum("probability_source", [
|
||
"manual",
|
||
"futures_odds",
|
||
"elo_simulation",
|
||
"performance_model",
|
||
]);
|
||
|
||
export const simulationStatusEnum = pgEnum("simulation_status", [
|
||
"idle",
|
||
"running",
|
||
"failed",
|
||
]);
|
||
|
||
export const simulatorTypeEnum = pgEnum("simulator_type", [
|
||
"f1_standings",
|
||
"indycar_standings",
|
||
"golf_qualifying_points",
|
||
"playoff_bracket",
|
||
"ucl_bracket",
|
||
"ncaam_bracket",
|
||
"ncaaw_bracket",
|
||
"nba_bracket",
|
||
"nhl_bracket",
|
||
"nfl_bracket",
|
||
"afl_bracket",
|
||
"epl_standings",
|
||
"snooker_bracket",
|
||
"tennis_qualifying_points",
|
||
"mlb_bracket",
|
||
"wnba_bracket",
|
||
"world_cup",
|
||
"darts_bracket",
|
||
"cs2_major_qualifying_points",
|
||
"ncaa_football_bracket",
|
||
"llws_bracket",
|
||
"college_hockey_bracket",
|
||
"brackt",
|
||
]);
|
||
|
||
export const tournamentStatusEnum = pgEnum("tournament_status", [
|
||
"scheduled",
|
||
"in_progress",
|
||
"completed",
|
||
]);
|
||
|
||
export const playoffMatchGameStatusEnum = pgEnum("playoff_match_game_status", [
|
||
"scheduled",
|
||
"complete",
|
||
"postponed",
|
||
]);
|
||
|
||
export const leagues = pgTable("leagues", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
name: varchar("name", { length: 255 }).notNull(),
|
||
createdBy: varchar("created_by", { length: 255 }).notNull(), // UUID → users.id
|
||
currentSeasonId: uuid("current_season_id"), // References the active season
|
||
isPublicDraftBoard: boolean("is_public_draft_board").notNull().default(false),
|
||
discordWebhookUrl: text("discord_webhook_url"),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
});
|
||
|
||
export const seasonTemplates = pgTable("season_templates", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
name: varchar("name", { length: 255 }).notNull(),
|
||
description: text("description"),
|
||
year: integer("year").notNull(),
|
||
isActive: boolean("is_active").notNull().default(true),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
});
|
||
|
||
export const seasons = pgTable("seasons", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
leagueId: uuid("league_id")
|
||
.notNull()
|
||
.references(() => leagues.id, { onDelete: "cascade" }),
|
||
year: integer("year").notNull(),
|
||
status: seasonStatusEnum("status").notNull().default("pre_draft"),
|
||
templateId: uuid("template_id")
|
||
.references(() => seasonTemplates.id, { onDelete: "set null" }),
|
||
draftRounds: integer("draft_rounds").notNull().default(20),
|
||
flexSpots: integer("flex_spots").notNull().default(0),
|
||
draftDateTime: timestamp("draft_date_time"),
|
||
draftInitialTime: integer("draft_initial_time").notNull().default(120), // seconds
|
||
draftIncrementTime: integer("draft_increment_time").notNull().default(30), // seconds
|
||
draftTimerMode: draftTimerModeEnum("draft_timer_mode").notNull().default("chess_clock"),
|
||
currentPickNumber: integer("current_pick_number").default(1),
|
||
draftStartedAt: timestamp("draft_started_at"),
|
||
draftCompletedAt: timestamp("draft_completed_at"),
|
||
draftPaused: boolean("draft_paused").notNull().default(false),
|
||
overnightPauseMode: overnightPauseModeEnum("overnight_pause_mode").notNull().default("none"),
|
||
overnightPauseStart: varchar("overnight_pause_start", { length: 5 }), // "23:00"
|
||
overnightPauseEnd: varchar("overnight_pause_end", { length: 5 }), // "07:00"
|
||
overnightPauseTimezone: varchar("overnight_pause_timezone", { length: 50 }), // IANA — league TZ (league mode) or per-user fallback
|
||
inviteCode: varchar("invite_code", { length: 20 }).notNull().unique(),
|
||
// Scoring configuration (8 values for 1st through 8th)
|
||
pointsFor1st: integer("points_for_1st").notNull().default(100),
|
||
pointsFor2nd: integer("points_for_2nd").notNull().default(70),
|
||
pointsFor3rd: integer("points_for_3rd").notNull().default(50),
|
||
pointsFor4th: integer("points_for_4th").notNull().default(40),
|
||
pointsFor5th: integer("points_for_5th").notNull().default(25),
|
||
pointsFor6th: integer("points_for_6th").notNull().default(25),
|
||
pointsFor7th: integer("points_for_7th").notNull().default(15),
|
||
pointsFor8th: integer("points_for_8th").notNull().default(15),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
});
|
||
|
||
export const teams = pgTable("teams", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
seasonId: uuid("season_id")
|
||
.notNull()
|
||
.references(() => seasons.id, { onDelete: "cascade" }),
|
||
name: varchar("name", { length: 255 }).notNull(),
|
||
logoUrl: varchar("logo_url", { length: 512 }),
|
||
flagConfig: jsonb("flag_config").$type<{ pattern: string; colors: string[] }>(),
|
||
avatarType: varchar("avatar_type", { length: 20 }).notNull().default("owner"),
|
||
ownerId: varchar("owner_id", { length: 255 }), // UUID → users.id, nullable
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
});
|
||
|
||
export const draftSlots = pgTable("draft_slots", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
seasonId: uuid("season_id")
|
||
.notNull()
|
||
.references(() => seasons.id, { onDelete: "cascade" }),
|
||
teamId: uuid("team_id")
|
||
.notNull()
|
||
.references(() => teams.id, { onDelete: "cascade" }),
|
||
draftOrder: integer("draft_order").notNull(),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
});
|
||
|
||
export const draftPicks = pgTable("draft_picks", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
seasonId: uuid("season_id")
|
||
.notNull()
|
||
.references(() => seasons.id, { onDelete: "cascade" }),
|
||
teamId: uuid("team_id")
|
||
.notNull()
|
||
.references(() => teams.id, { onDelete: "cascade" }),
|
||
participantId: uuid("participant_id")
|
||
.notNull()
|
||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||
pickNumber: integer("pick_number").notNull(),
|
||
round: integer("round").notNull(),
|
||
pickInRound: integer("pick_in_round").notNull(),
|
||
pickedByUserId: varchar("picked_by_user_id", { length: 255 }).notNull(), // UUID → users.id
|
||
pickedByType: pickedByTypeEnum("picked_by_type").notNull(),
|
||
timeUsed: integer("time_used").notNull().default(0), // team's time bank (seconds) at the moment the pick was made
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
}, (t) => ({
|
||
// Prevents two concurrent picks from landing in the same slot
|
||
uniquePickSlot: uniqueIndex("draft_picks_season_pick_unique").on(t.seasonId, t.pickNumber),
|
||
}));
|
||
|
||
export const draftQueue = pgTable("draft_queue", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
seasonId: uuid("season_id")
|
||
.notNull()
|
||
.references(() => seasons.id, { onDelete: "cascade" }),
|
||
teamId: uuid("team_id")
|
||
.notNull()
|
||
.references(() => teams.id, { onDelete: "cascade" }),
|
||
participantId: uuid("participant_id")
|
||
.notNull()
|
||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||
queuePosition: integer("queue_position").notNull(),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
});
|
||
|
||
export const watchlist = pgTable("watchlist", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
seasonId: uuid("season_id")
|
||
.notNull()
|
||
.references(() => seasons.id, { onDelete: "cascade" }),
|
||
teamId: uuid("team_id")
|
||
.notNull()
|
||
.references(() => teams.id, { onDelete: "cascade" }),
|
||
participantId: uuid("participant_id")
|
||
.notNull()
|
||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
}, (t) => ({
|
||
uniqueWatch: uniqueIndex("watchlist_season_team_participant_unique").on(t.seasonId, t.teamId, t.participantId),
|
||
}));
|
||
|
||
export const draftTimers = pgTable("draft_timers", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
seasonId: uuid("season_id")
|
||
.notNull()
|
||
.references(() => seasons.id, { onDelete: "cascade" }),
|
||
teamId: uuid("team_id")
|
||
.notNull()
|
||
.references(() => teams.id, { onDelete: "cascade" }),
|
||
timeRemaining: integer("time_remaining").notNull(), // seconds
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
});
|
||
|
||
export const autodraftSettings = pgTable("autodraft_settings", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
seasonId: uuid("season_id")
|
||
.notNull()
|
||
.references(() => seasons.id, { onDelete: "cascade" }),
|
||
teamId: uuid("team_id")
|
||
.notNull()
|
||
.references(() => teams.id, { onDelete: "cascade" }),
|
||
isEnabled: boolean("is_enabled").notNull().default(false),
|
||
mode: autodraftModeEnum("mode").notNull().default("next_pick"),
|
||
queueOnly: boolean("queue_only").notNull().default(false),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
});
|
||
|
||
export const commissioners = pgTable("commissioners", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
leagueId: uuid("league_id")
|
||
.notNull()
|
||
.references(() => leagues.id, { onDelete: "cascade" }),
|
||
userId: varchar("user_id", { length: 255 }).notNull(), // UUID → users.id
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
});
|
||
|
||
// Sports Tables
|
||
export const sports = pgTable("sports", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
name: varchar("name", { length: 255 }).notNull(),
|
||
type: sportTypeEnum("type").notNull(),
|
||
slug: varchar("slug", { length: 255 }).notNull().unique(),
|
||
description: text("description"),
|
||
iconUrl: varchar("icon_url", { length: 512 }), // Cloudinary URL or legacy filename in /public/sports-icons/
|
||
simulatorType: simulatorTypeEnum("simulator_type"),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
});
|
||
|
||
export const sportsSeasons = pgTable("sports_seasons", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
sportId: uuid("sport_id")
|
||
.notNull()
|
||
.references(() => sports.id, { onDelete: "cascade" }),
|
||
fantasySeasonId: uuid("fantasy_season_id").references(() => seasons.id, {
|
||
onDelete: "cascade",
|
||
}),
|
||
name: varchar("name", { length: 255 }).notNull(),
|
||
year: integer("year").notNull(),
|
||
startDate: date("start_date"),
|
||
endDate: date("end_date"),
|
||
status: sportsSeasonStatusEnum("status").notNull().default("upcoming"),
|
||
scoringType: scoringTypeEnum("scoring_type").notNull(),
|
||
// New scoring pattern field (replaces/augments scoringType)
|
||
scoringPattern: scoringPatternEnum("scoring_pattern"),
|
||
// For qualifying points sports
|
||
totalMajors: integer("total_majors"),
|
||
majorsCompleted: integer("majors_completed").notNull().default(0),
|
||
qualifyingPointsFinalized: boolean("qualifying_points_finalized").notNull().default(false),
|
||
// EV Calibration parameters (for Elo-based probability generation)
|
||
eloCalibrationExponent: decimal("elo_calibration_exponent", { precision: 3, scale: 2 }), // e.g., 0.33
|
||
eloMinRating: integer("elo_min_rating").default(1250),
|
||
eloMaxRating: integer("elo_max_rating").default(1750),
|
||
// Simulation status (prevents concurrent simulation runs)
|
||
simulationStatus: simulationStatusEnum("simulation_status").notNull().default("idle"),
|
||
// Controls the window during which this sport season appears in league creation/settings
|
||
draftOn: date("draft_on").notNull(),
|
||
draftOff: date("draft_off").notNull(),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
});
|
||
|
||
export const simulatorProfiles = pgTable("simulator_profiles", {
|
||
simulatorType: simulatorTypeEnum("simulator_type").primaryKey(),
|
||
displayName: varchar("display_name", { length: 255 }).notNull(),
|
||
description: text("description"),
|
||
defaultConfig: jsonb("default_config").$type<Record<string, unknown>>().notNull().default({}),
|
||
inputSchema: jsonb("input_schema").$type<Record<string, unknown>>().notNull().default({}),
|
||
isActive: boolean("is_active").notNull().default(true),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
});
|
||
|
||
export const sportsSeasonSimulatorConfigs = pgTable("sports_season_simulator_configs", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
sportsSeasonId: uuid("sports_season_id")
|
||
.notNull()
|
||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||
simulatorType: simulatorTypeEnum("simulator_type").notNull(),
|
||
config: jsonb("config").$type<Record<string, unknown>>().notNull().default({}),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
}, (table) => ({
|
||
uniqueSportsSeason: uniqueIndex("sports_season_simulator_config_unique").on(table.sportsSeasonId),
|
||
simulatorTypeIdx: index("sports_season_simulator_config_type_idx").on(table.simulatorType),
|
||
}));
|
||
|
||
export const seasonParticipants = pgTable("season_participants", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
sportsSeasonId: uuid("sports_season_id")
|
||
.notNull()
|
||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||
participantId: uuid("participant_id").references(() => participants.id, {
|
||
onDelete: "restrict",
|
||
}),
|
||
name: varchar("name", { length: 255 }).notNull(),
|
||
shortName: varchar("short_name", { length: 100 }),
|
||
externalId: varchar("external_id", { length: 255 }),
|
||
expectedValue: decimal("expected_value", { precision: 10, scale: 4 }).notNull().default("0"),
|
||
vorpValue: decimal("vorp_value", { precision: 10, scale: 4 }).notNull().default("0"),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
}, (table) => [
|
||
uniqueIndex("participants_sports_season_name_unique").on(table.sportsSeasonId, table.name),
|
||
]);
|
||
|
||
export const seasonParticipantSimulatorInputs = pgTable("season_participant_simulator_inputs", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
participantId: uuid("participant_id")
|
||
.notNull()
|
||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||
sportsSeasonId: uuid("sports_season_id")
|
||
.notNull()
|
||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||
sourceOdds: integer("source_odds"),
|
||
sourceElo: integer("source_elo"),
|
||
worldRanking: integer("world_ranking"),
|
||
rating: decimal("rating", { precision: 10, scale: 4 }),
|
||
projectedWins: decimal("projected_wins", { precision: 6, scale: 2 }),
|
||
projectedTablePoints: decimal("projected_table_points", { precision: 6, scale: 2 }),
|
||
seed: integer("seed"),
|
||
region: varchar("region", { length: 100 }),
|
||
metadata: jsonb("metadata").$type<Record<string, unknown>>(),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
}, (table) => ({
|
||
uniqueParticipantSeason: uniqueIndex("season_participant_simulator_inputs_unique").on(
|
||
table.participantId,
|
||
table.sportsSeasonId,
|
||
),
|
||
sportsSeasonIdx: index("season_participant_simulator_inputs_season_idx").on(table.sportsSeasonId),
|
||
}));
|
||
|
||
export const participants = pgTable(
|
||
"participants",
|
||
{
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
sportId: uuid("sport_id")
|
||
.notNull()
|
||
.references(() => sports.id, { onDelete: "cascade" }),
|
||
name: varchar("name", { length: 255 }).notNull(),
|
||
externalKey: varchar("external_key", { length: 255 }),
|
||
metadata: jsonb("metadata"),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
},
|
||
(t) => ({
|
||
uniqueSportName: uniqueIndex("participants_sport_name_unique").on(
|
||
t.sportId,
|
||
t.name,
|
||
),
|
||
}),
|
||
);
|
||
|
||
export const tournaments = pgTable(
|
||
"tournaments",
|
||
{
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
sportId: uuid("sport_id")
|
||
.notNull()
|
||
.references(() => sports.id, { onDelete: "cascade" }),
|
||
name: varchar("name", { length: 255 }).notNull(),
|
||
year: integer("year").notNull(),
|
||
startsAt: timestamp("starts_at"),
|
||
endsAt: timestamp("ends_at"),
|
||
surface: varchar("surface", { length: 50 }), // tennis only: hard | clay | grass
|
||
location: varchar("location", { length: 255 }),
|
||
status: tournamentStatusEnum("status").notNull().default("scheduled"),
|
||
externalKey: varchar("external_key", { length: 255 }),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
},
|
||
(t) => ({
|
||
uniqueSportNameYear: uniqueIndex("tournaments_sport_name_year_unique").on(
|
||
t.sportId,
|
||
t.name,
|
||
t.year,
|
||
),
|
||
}),
|
||
);
|
||
|
||
export const tournamentResults = pgTable(
|
||
"tournament_results",
|
||
{
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
tournamentId: uuid("tournament_id")
|
||
.notNull()
|
||
.references(() => tournaments.id, { onDelete: "cascade" }),
|
||
participantId: uuid("participant_id")
|
||
.notNull()
|
||
.references(() => participants.id, { onDelete: "cascade" }),
|
||
placement: integer("placement"),
|
||
rawScore: decimal("raw_score", { precision: 10, scale: 2 }),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
},
|
||
(t) => ({
|
||
uniqueTournamentParticipant: uniqueIndex(
|
||
"tournament_results_tournament_participant_unique",
|
||
).on(t.tournamentId, t.participantId),
|
||
}),
|
||
);
|
||
|
||
export const participantSurfaceElos = pgTable(
|
||
"participant_surface_elos",
|
||
{
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
participantId: uuid("participant_id")
|
||
.notNull()
|
||
.references(() => participants.id, { onDelete: "cascade" }),
|
||
worldRanking: integer("world_ranking"),
|
||
eloHard: integer("elo_hard"),
|
||
eloClay: integer("elo_clay"),
|
||
eloGrass: integer("elo_grass"),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
},
|
||
(t) => ({
|
||
uniqueParticipant: uniqueIndex("participant_surface_elos_participant_unique").on(
|
||
t.participantId,
|
||
),
|
||
}),
|
||
);
|
||
|
||
export const seasonTemplateSports = pgTable("season_template_sports", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
templateId: uuid("template_id")
|
||
.notNull()
|
||
.references(() => seasonTemplates.id, { onDelete: "cascade" }),
|
||
sportsSeasonId: uuid("sports_season_id")
|
||
.notNull()
|
||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
});
|
||
|
||
export const seasonSports = pgTable("season_sports", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
seasonId: uuid("season_id")
|
||
.notNull()
|
||
.references(() => seasons.id, { onDelete: "cascade" }),
|
||
sportsSeasonId: uuid("sports_season_id")
|
||
.notNull()
|
||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
});
|
||
|
||
export const seasonParticipantResults = pgTable("season_participant_results", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
participantId: uuid("participant_id")
|
||
.notNull()
|
||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||
sportsSeasonId: uuid("sports_season_id")
|
||
.notNull()
|
||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||
finalPosition: integer("final_position"),
|
||
isPartialScore: boolean("is_partial_score").notNull().default(false),
|
||
qualifyingPoints: decimal("qualifying_points", { precision: 10, scale: 2 }),
|
||
notes: text("notes"),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
});
|
||
|
||
// Scoring System Tables
|
||
|
||
// Individual scoring events (games, tournaments, races)
|
||
export const scoringEvents = pgTable("scoring_events", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
sportsSeasonId: uuid("sports_season_id")
|
||
.notNull()
|
||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||
tournamentId: uuid("tournament_id").references(() => tournaments.id, {
|
||
onDelete: "set null",
|
||
}),
|
||
name: varchar("name", { length: 255 }).notNull(), // "2024 Masters", "Super Bowl LIX", etc.
|
||
eventDate: date("event_date"),
|
||
eventStartsAt: timestamp("event_starts_at", { withTimezone: true }),
|
||
eventType: eventTypeEnum("event_type").notNull(),
|
||
// For playoff events
|
||
playoffRound: varchar("playoff_round", { length: 50 }), // "Quarterfinals", "Semifinals", "Finals"
|
||
// For qualifying events
|
||
isQualifyingEvent: boolean("is_qualifying_event").notNull().default(false),
|
||
// Template system (Phase 2.6)
|
||
bracketTemplateId: varchar("bracket_template_id", { length: 50 }), // "ncaa_68", "nfl_14", "simple_16", etc.
|
||
scoringStartsAtRound: varchar("scoring_starts_at_round", { length: 50 }), // "Elite Eight", "Quarterfinals", etc.
|
||
// Per-event region config for NCAA-style brackets (overrides template defaults)
|
||
bracketRegionConfig: jsonb("bracket_region_config"), // BracketRegion[] | null
|
||
isComplete: boolean("is_complete").notNull().default(false),
|
||
completedAt: timestamp("completed_at"),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
}, (t) => ({
|
||
qualifyingEventsRequireTournament: check(
|
||
"scoring_events_qualifying_require_tournament",
|
||
sql`NOT ${t.isQualifyingEvent} OR ${t.tournamentId} IS NOT NULL`,
|
||
),
|
||
}));
|
||
|
||
// Results for participants in specific events
|
||
export const eventResults = pgTable("event_results", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
scoringEventId: uuid("scoring_event_id")
|
||
.notNull()
|
||
.references(() => scoringEvents.id, { onDelete: "cascade" }),
|
||
seasonParticipantId: uuid("season_participant_id")
|
||
.notNull()
|
||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||
// Placement in this specific event
|
||
placement: integer("placement"),
|
||
// For qualifying events: QP awarded in this event
|
||
qualifyingPointsAwarded: decimal("qualifying_points_awarded", { precision: 10, scale: 2 }),
|
||
// For playoff events: eliminated or advanced
|
||
eliminated: boolean("eliminated"),
|
||
// Raw sport-specific data (optional)
|
||
rawScore: decimal("raw_score", { precision: 10, scale: 2 }), // strokes, points, time, etc.
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
}, (t) => ({
|
||
uniqueEventParticipant: uniqueIndex("event_results_event_participant_unique").on(
|
||
t.scoringEventId,
|
||
t.seasonParticipantId,
|
||
),
|
||
}));
|
||
|
||
// Playoff bracket matches (for display and tracking)
|
||
export const playoffMatches = pgTable("playoff_matches", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
scoringEventId: uuid("scoring_event_id")
|
||
.notNull()
|
||
.references(() => scoringEvents.id, { onDelete: "cascade" }),
|
||
round: varchar("round", { length: 50 }).notNull(), // "Quarterfinals", "Semifinals", "Finals"
|
||
matchNumber: integer("match_number").notNull(), // 1, 2, 3, 4 (for QF); 1, 2 (for SF); 1 (for F)
|
||
participant1Id: uuid("participant1_id").references(() => seasonParticipants.id, { onDelete: "set null" }),
|
||
participant2Id: uuid("participant2_id").references(() => seasonParticipants.id, { onDelete: "set null" }),
|
||
winnerId: uuid("winner_id").references(() => seasonParticipants.id, { onDelete: "set null" }),
|
||
loserId: uuid("loser_id").references(() => seasonParticipants.id, { onDelete: "set null" }),
|
||
isComplete: boolean("is_complete").notNull().default(false),
|
||
// Optional detailed data
|
||
participant1Score: decimal("participant1_score", { precision: 10, scale: 2 }),
|
||
participant2Score: decimal("participant2_score", { precision: 10, scale: 2 }),
|
||
// Template system (Phase 2.6)
|
||
isScoring: boolean("is_scoring").notNull().default(true), // Does this match affect fantasy points?
|
||
templateRound: varchar("template_round", { length: 50 }), // "First Four", "Round of 64", "Elite Eight", etc.
|
||
seedInfo: varchar("seed_info", { length: 50 }), // "1 vs 16", "11a vs 11b", etc. (for display)
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
});
|
||
|
||
// Individual games within a playoff matchup (e.g., NBA 7-game series games)
|
||
export const playoffMatchGames = pgTable("playoff_match_games", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
playoffMatchId: uuid("playoff_match_id")
|
||
.notNull()
|
||
.references(() => playoffMatches.id, { onDelete: "cascade" }),
|
||
gameNumber: integer("game_number").notNull(),
|
||
scheduledAt: timestamp("scheduled_at"),
|
||
status: playoffMatchGameStatusEnum("status").notNull().default("scheduled"),
|
||
participant1Score: decimal("participant1_score", { precision: 10, scale: 2 }),
|
||
participant2Score: decimal("participant2_score", { precision: 10, scale: 2 }),
|
||
winnerId: uuid("winner_id").references(() => seasonParticipants.id, { onDelete: "set null" }),
|
||
notes: text("notes"),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
});
|
||
|
||
// Moneyline odds per participant per matchup (single record per participant, overwritten on update)
|
||
export const playoffMatchOdds = pgTable("playoff_match_odds", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
playoffMatchId: uuid("playoff_match_id")
|
||
.notNull()
|
||
.references(() => playoffMatches.id, { onDelete: "cascade" }),
|
||
participantId: uuid("participant_id")
|
||
.notNull()
|
||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||
moneylineOdds: integer("moneyline_odds"), // American format e.g. -110, +150
|
||
impliedProbability: decimal("implied_probability", { precision: 6, scale: 4 }),
|
||
oddsSource: varchar("odds_source", { length: 100 }),
|
||
recordedAt: timestamp("recorded_at").defaultNow().notNull(),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
});
|
||
|
||
// Aggregated participant qualifying points (for qualifying_points sports)
|
||
export const seasonParticipantQualifyingTotals = pgTable("season_participant_qualifying_totals", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
participantId: uuid("participant_id")
|
||
.notNull()
|
||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||
sportsSeasonId: uuid("sports_season_id")
|
||
.notNull()
|
||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||
totalQualifyingPoints: decimal("total_qualifying_points", { precision: 10, scale: 2 }).notNull().default("0"),
|
||
eventsScored: integer("events_scored").notNull().default(0),
|
||
// After finalization
|
||
finalRanking: integer("final_ranking"), // 1-8 based on QP totals
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
});
|
||
|
||
// Configurable qualifying point values per sports season
|
||
export const qualifyingPointConfig = pgTable("qualifying_point_config", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
sportsSeasonId: uuid("sports_season_id")
|
||
.notNull()
|
||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||
placement: integer("placement").notNull(), // 1-16
|
||
points: decimal("points", { precision: 10, scale: 2 }).notNull(),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
});
|
||
|
||
// Team scores aggregated by sport season
|
||
export const teamSportScores = pgTable("team_sport_scores", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
teamId: uuid("team_id")
|
||
.notNull()
|
||
.references(() => teams.id, { onDelete: "cascade" }),
|
||
sportsSeasonId: uuid("sports_season_id")
|
||
.notNull()
|
||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||
totalPoints: decimal("total_points", { precision: 10, scale: 2 }).notNull().default("0"),
|
||
participantsCompleted: integer("participants_completed").notNull().default(0), // How many picks have finished
|
||
participantsTotal: integer("participants_total").notNull().default(0), // Total picks for this sport
|
||
calculatedAt: timestamp("calculated_at").defaultNow().notNull(),
|
||
});
|
||
|
||
// Overall team standings (snapshot-based for movement tracking)
|
||
export const teamStandings = pgTable("team_standings", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
teamId: uuid("team_id")
|
||
.notNull()
|
||
.references(() => teams.id, { onDelete: "cascade" }),
|
||
seasonId: uuid("season_id")
|
||
.notNull()
|
||
.references(() => seasons.id, { onDelete: "cascade" }),
|
||
totalPoints: decimal("total_points", { precision: 10, scale: 2 }).notNull().default("0"),
|
||
currentRank: integer("current_rank").notNull(),
|
||
previousRank: integer("previous_rank"), // For movement indicators
|
||
// Tiebreaker data
|
||
firstPlaceCount: integer("first_place_count").notNull().default(0),
|
||
secondPlaceCount: integer("second_place_count").notNull().default(0),
|
||
thirdPlaceCount: integer("third_place_count").notNull().default(0),
|
||
fourthPlaceCount: integer("fourth_place_count").notNull().default(0),
|
||
fifthPlaceCount: integer("fifth_place_count").notNull().default(0),
|
||
sixthPlaceCount: integer("sixth_place_count").notNull().default(0),
|
||
seventhPlaceCount: integer("seventh_place_count").notNull().default(0),
|
||
eighthPlaceCount: integer("eighth_place_count").notNull().default(0),
|
||
participantsRemaining: integer("participants_remaining").notNull().default(0), // Not yet finished
|
||
// Expected value tracking (Phase 5.4)
|
||
actualPoints: decimal("actual_points", { precision: 10, scale: 2 }), // Points from finished participants
|
||
projectedPoints: decimal("projected_points", { precision: 10, scale: 2 }), // actualPoints + EVs of unfinished
|
||
participantsFinished: integer("participants_finished"), // Count of finished participants
|
||
calculatedAt: timestamp("calculated_at").defaultNow().notNull(),
|
||
});
|
||
|
||
// Daily standings snapshots for historical tracking
|
||
export const teamStandingsSnapshots = pgTable("team_standings_snapshots", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
teamId: uuid("team_id")
|
||
.notNull()
|
||
.references(() => teams.id, { onDelete: "cascade" }),
|
||
seasonId: uuid("season_id")
|
||
.notNull()
|
||
.references(() => seasons.id, { onDelete: "cascade" }),
|
||
snapshotDate: date("snapshot_date").notNull(),
|
||
totalPoints: decimal("total_points", { precision: 10, scale: 2 }).notNull().default("0"),
|
||
rank: integer("rank").notNull(),
|
||
// Tiebreaker data
|
||
firstPlaceCount: integer("first_place_count").notNull().default(0),
|
||
secondPlaceCount: integer("second_place_count").notNull().default(0),
|
||
thirdPlaceCount: integer("third_place_count").notNull().default(0),
|
||
fourthPlaceCount: integer("fourth_place_count").notNull().default(0),
|
||
fifthPlaceCount: integer("fifth_place_count").notNull().default(0),
|
||
sixthPlaceCount: integer("sixth_place_count").notNull().default(0),
|
||
seventhPlaceCount: integer("seventh_place_count").notNull().default(0),
|
||
eighthPlaceCount: integer("eighth_place_count").notNull().default(0),
|
||
participantsRemaining: integer("participants_remaining").notNull().default(0),
|
||
// Expected value tracking
|
||
actualPoints: decimal("actual_points", { precision: 10, scale: 2 }), // Points from finished participants
|
||
projectedPoints: decimal("projected_points", { precision: 10, scale: 2 }), // actualPoints + EVs of unfinished
|
||
participantsFinished: integer("participants_finished"), // Count of finished participants
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
}, (t) => ({
|
||
uniqueTeamSeasonDate: uniqueIndex("team_standings_snapshots_unique").on(
|
||
t.teamId, t.seasonId, t.snapshotDate
|
||
),
|
||
}));
|
||
|
||
// Expected value tracking (sports-season-specific)
|
||
export const seasonParticipantExpectedValues = pgTable("season_participant_expected_values", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
participantId: uuid("participant_id")
|
||
.notNull()
|
||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||
sportsSeasonId: uuid("sports_season_id")
|
||
.notNull()
|
||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||
// Probability distribution (stored as fractions, e.g. 0.1550 = 15.5%)
|
||
probFirst: decimal("prob_first", { precision: 6, scale: 4 }).notNull().default("0"),
|
||
probSecond: decimal("prob_second", { precision: 6, scale: 4 }).notNull().default("0"),
|
||
probThird: decimal("prob_third", { precision: 6, scale: 4 }).notNull().default("0"),
|
||
probFourth: decimal("prob_fourth", { precision: 6, scale: 4 }).notNull().default("0"),
|
||
probFifth: decimal("prob_fifth", { precision: 6, scale: 4 }).notNull().default("0"),
|
||
probSixth: decimal("prob_sixth", { precision: 6, scale: 4 }).notNull().default("0"),
|
||
probSeventh: decimal("prob_seventh", { precision: 6, scale: 4 }).notNull().default("0"),
|
||
probEighth: decimal("prob_eighth", { precision: 6, scale: 4 }).notNull().default("0"),
|
||
// Calculated EV
|
||
expectedValue: decimal("expected_value", { precision: 10, scale: 4 }).notNull().default("0"),
|
||
// Metadata
|
||
source: probabilitySourceEnum("source").default("manual"), // How probabilities were generated
|
||
sourceOdds: integer("source_odds"), // Original odds if source is futures_odds (American odds format)
|
||
sourceElo: integer("source_elo"), // Raw Elo rating if source is elo-based simulator (e.g. snooker_bracket, darts_bracket)
|
||
worldRanking: integer("world_ranking"), // World ranking for sports that use it for bracket seeding (e.g. darts_bracket)
|
||
calculatedAt: timestamp("calculated_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
}, (t) => ({
|
||
uniqueParticipantSeason: uniqueIndex("participant_ev_participant_season_unique").on(
|
||
t.participantId, t.sportsSeasonId
|
||
),
|
||
}));
|
||
|
||
// Tournament group stage tables (for FIFA World Cup style tournaments)
|
||
export const tournamentGroups = pgTable("tournament_groups", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
scoringEventId: uuid("scoring_event_id")
|
||
.notNull()
|
||
.references(() => scoringEvents.id, { onDelete: "cascade" }),
|
||
groupName: varchar("group_name", { length: 10 }).notNull(), // "A" through "L"
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
});
|
||
|
||
export const tournamentGroupMembers = pgTable("tournament_group_members", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
tournamentGroupId: uuid("tournament_group_id")
|
||
.notNull()
|
||
.references(() => tournamentGroups.id, { onDelete: "cascade" }),
|
||
participantId: uuid("participant_id")
|
||
.notNull()
|
||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||
eliminated: boolean("eliminated").notNull().default(false),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
});
|
||
|
||
export const groupStageMatches = pgTable("group_stage_matches", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
tournamentGroupId: uuid("tournament_group_id")
|
||
.notNull()
|
||
.references(() => tournamentGroups.id, { onDelete: "cascade" }),
|
||
participant1Id: uuid("participant1_id")
|
||
.notNull()
|
||
.references(() => seasonParticipants.id),
|
||
participant2Id: uuid("participant2_id")
|
||
.notNull()
|
||
.references(() => seasonParticipants.id),
|
||
participant1Score: integer("participant1_score"),
|
||
participant2Score: integer("participant2_score"),
|
||
isComplete: boolean("is_complete").notNull().default(false),
|
||
matchday: integer("matchday").notNull(), // 1, 2, or 3 (round of group play)
|
||
scheduledAt: timestamp("scheduled_at"),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
}, (t) => [
|
||
// Prevent duplicate pairings within the same group (order-independent)
|
||
uniqueIndex("group_stage_matches_group_pair_unique")
|
||
.on(t.tournamentGroupId, t.participant1Id, t.participant2Id),
|
||
]);
|
||
|
||
// Participant season results (for F1 current points tracking during season)
|
||
export const participantSeasonResults = pgTable("participant_season_results", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
participantId: uuid("participant_id")
|
||
.notNull()
|
||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||
sportsSeasonId: uuid("sports_season_id")
|
||
.notNull()
|
||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||
currentPoints: decimal("current_points", { precision: 10, scale: 2 }).notNull().default("0"), // Current F1 championship points, etc.
|
||
currentPosition: integer("current_position"), // Current standings position
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
});
|
||
|
||
// EV snapshots — historical record of participant EV over time (one per participant per sport season per day)
|
||
export const participantEvSnapshots = pgTable("participant_ev_snapshots", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
participantId: uuid("participant_id")
|
||
.notNull()
|
||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||
sportsSeasonId: uuid("sports_season_id")
|
||
.notNull()
|
||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||
snapshotDate: date("snapshot_date").notNull(),
|
||
// Probability distribution (stored as fractions, e.g. 0.1550 = 15.5%)
|
||
probFirst: decimal("prob_first", { precision: 5, scale: 2 }).notNull().default("0"),
|
||
probSecond: decimal("prob_second", { precision: 5, scale: 2 }).notNull().default("0"),
|
||
probThird: decimal("prob_third", { precision: 5, scale: 2 }).notNull().default("0"),
|
||
probFourth: decimal("prob_fourth", { precision: 5, scale: 2 }).notNull().default("0"),
|
||
probFifth: decimal("prob_fifth", { precision: 5, scale: 2 }).notNull().default("0"),
|
||
probSixth: decimal("prob_sixth", { precision: 5, scale: 2 }).notNull().default("0"),
|
||
probSeventh: decimal("prob_seventh", { precision: 5, scale: 2 }).notNull().default("0"),
|
||
probEighth: decimal("prob_eighth", { precision: 5, scale: 2 }).notNull().default("0"),
|
||
calculatedEV: decimal("calculated_ev", { precision: 10, scale: 4 }).notNull().default("0"),
|
||
source: varchar("source", { length: 100 }).notNull(), // e.g. 'bracket_monte_carlo', 'f1_standings_model'
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
}, (t) => ({
|
||
uniqueParticipantSeasonDate: uniqueIndex("participant_ev_snapshots_unique").on(
|
||
t.participantId, t.sportsSeasonId, t.snapshotDate
|
||
),
|
||
}));
|
||
|
||
// Team EV snapshots — historical projected points per team per fantasy season per day
|
||
export const teamEvSnapshots = pgTable("team_ev_snapshots", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
teamId: uuid("team_id")
|
||
.notNull()
|
||
.references(() => teams.id, { onDelete: "cascade" }),
|
||
seasonId: uuid("season_id")
|
||
.notNull()
|
||
.references(() => seasons.id, { onDelete: "cascade" }),
|
||
snapshotDate: date("snapshot_date").notNull(),
|
||
projectedPoints: decimal("projected_points", { precision: 10, scale: 2 }).notNull().default("0"),
|
||
actualPoints: decimal("actual_points", { precision: 10, scale: 2 }).notNull().default("0"),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
}, (t) => ({
|
||
uniqueTeamSeasonDate: uniqueIndex("team_ev_snapshots_unique").on(
|
||
t.teamId, t.seasonId, t.snapshotDate
|
||
),
|
||
}));
|
||
|
||
// Relations
|
||
export const sportsRelations = relations(sports, ({ many }) => ({
|
||
sportsSeasons: many(sportsSeasons),
|
||
tournaments: many(tournaments),
|
||
participants: many(participants),
|
||
}));
|
||
|
||
export const sportsSeasonsRelations = relations(sportsSeasons, ({ one, many }) => ({
|
||
sport: one(sports, {
|
||
fields: [sportsSeasons.sportId],
|
||
references: [sports.id],
|
||
}),
|
||
fantasySeason: one(seasons, {
|
||
fields: [sportsSeasons.fantasySeasonId],
|
||
references: [seasons.id],
|
||
}),
|
||
participants: many(seasonParticipants),
|
||
simulatorConfig: one(sportsSeasonSimulatorConfigs),
|
||
simulatorInputs: many(seasonParticipantSimulatorInputs),
|
||
seasonTemplateSports: many(seasonTemplateSports),
|
||
seasonSports: many(seasonSports),
|
||
participantResults: many(seasonParticipantResults),
|
||
regularSeasonStandings: many(regularSeasonStandings),
|
||
pendingStandingsMappings: many(pendingStandingsMappings),
|
||
}));
|
||
|
||
export const simulatorProfilesRelations = relations(simulatorProfiles, ({ many }) => ({
|
||
sportsSeasonConfigs: many(sportsSeasonSimulatorConfigs),
|
||
}));
|
||
|
||
export const sportsSeasonSimulatorConfigsRelations = relations(sportsSeasonSimulatorConfigs, ({ one }) => ({
|
||
sportsSeason: one(sportsSeasons, {
|
||
fields: [sportsSeasonSimulatorConfigs.sportsSeasonId],
|
||
references: [sportsSeasons.id],
|
||
}),
|
||
profile: one(simulatorProfiles, {
|
||
fields: [sportsSeasonSimulatorConfigs.simulatorType],
|
||
references: [simulatorProfiles.simulatorType],
|
||
}),
|
||
}));
|
||
|
||
export const seasonParticipantsRelations = relations(seasonParticipants, ({ one, many }) => ({
|
||
sportsSeason: one(sportsSeasons, {
|
||
fields: [seasonParticipants.sportsSeasonId],
|
||
references: [sportsSeasons.id],
|
||
}),
|
||
canonicalParticipant: one(participants, {
|
||
fields: [seasonParticipants.participantId],
|
||
references: [participants.id],
|
||
}),
|
||
results: many(seasonParticipantResults),
|
||
regularSeasonStandings: many(regularSeasonStandings),
|
||
simulatorInput: one(seasonParticipantSimulatorInputs),
|
||
}));
|
||
|
||
export const seasonParticipantSimulatorInputsRelations = relations(seasonParticipantSimulatorInputs, ({ one }) => ({
|
||
participant: one(seasonParticipants, {
|
||
fields: [seasonParticipantSimulatorInputs.participantId],
|
||
references: [seasonParticipants.id],
|
||
}),
|
||
sportsSeason: one(sportsSeasons, {
|
||
fields: [seasonParticipantSimulatorInputs.sportsSeasonId],
|
||
references: [sportsSeasons.id],
|
||
}),
|
||
}));
|
||
|
||
export const participantsRelations = relations(participants, ({ one, many }) => ({
|
||
sport: one(sports, {
|
||
fields: [participants.sportId],
|
||
references: [sports.id],
|
||
}),
|
||
seasonParticipants: many(seasonParticipants),
|
||
tournamentResults: many(tournamentResults),
|
||
surfaceElo: one(participantSurfaceElos, {
|
||
fields: [participants.id],
|
||
references: [participantSurfaceElos.participantId],
|
||
}),
|
||
golfSkills: one(participantGolfSkills, {
|
||
fields: [participants.id],
|
||
references: [participantGolfSkills.participantId],
|
||
}),
|
||
}));
|
||
|
||
export const tournamentsRelations = relations(tournaments, ({ one, many }) => ({
|
||
sport: one(sports, {
|
||
fields: [tournaments.sportId],
|
||
references: [sports.id],
|
||
}),
|
||
scoringEvents: many(scoringEvents),
|
||
tournamentResults: many(tournamentResults),
|
||
}));
|
||
|
||
export const tournamentResultsRelations = relations(tournamentResults, ({ one }) => ({
|
||
tournament: one(tournaments, {
|
||
fields: [tournamentResults.tournamentId],
|
||
references: [tournaments.id],
|
||
}),
|
||
participant: one(participants, {
|
||
fields: [tournamentResults.participantId],
|
||
references: [participants.id],
|
||
}),
|
||
}));
|
||
|
||
export const participantSurfaceElosRelations = relations(
|
||
participantSurfaceElos,
|
||
({ one }) => ({
|
||
participant: one(participants, {
|
||
fields: [participantSurfaceElos.participantId],
|
||
references: [participants.id],
|
||
}),
|
||
}),
|
||
);
|
||
|
||
export const seasonTemplatesRelations = relations(seasonTemplates, ({ many }) => ({
|
||
seasonTemplateSports: many(seasonTemplateSports),
|
||
seasons: many(seasons),
|
||
}));
|
||
|
||
export const seasonTemplateSportsRelations = relations(seasonTemplateSports, ({ one }) => ({
|
||
template: one(seasonTemplates, {
|
||
fields: [seasonTemplateSports.templateId],
|
||
references: [seasonTemplates.id],
|
||
}),
|
||
sportsSeason: one(sportsSeasons, {
|
||
fields: [seasonTemplateSports.sportsSeasonId],
|
||
references: [sportsSeasons.id],
|
||
}),
|
||
}));
|
||
|
||
export const seasonsRelations = relations(seasons, ({ one, many }) => ({
|
||
league: one(leagues, {
|
||
fields: [seasons.leagueId],
|
||
references: [leagues.id],
|
||
}),
|
||
template: one(seasonTemplates, {
|
||
fields: [seasons.templateId],
|
||
references: [seasonTemplates.id],
|
||
}),
|
||
teams: many(teams),
|
||
seasonSports: many(seasonSports),
|
||
privateSportsSeasons: many(sportsSeasons),
|
||
}));
|
||
|
||
export const seasonSportsRelations = relations(seasonSports, ({ one }) => ({
|
||
season: one(seasons, {
|
||
fields: [seasonSports.seasonId],
|
||
references: [seasons.id],
|
||
}),
|
||
sportsSeason: one(sportsSeasons, {
|
||
fields: [seasonSports.sportsSeasonId],
|
||
references: [sportsSeasons.id],
|
||
}),
|
||
}));
|
||
|
||
export const seasonParticipantResultsRelations = relations(seasonParticipantResults, ({ one }) => ({
|
||
participant: one(seasonParticipants, {
|
||
fields: [seasonParticipantResults.participantId],
|
||
references: [seasonParticipants.id],
|
||
}),
|
||
sportsSeason: one(sportsSeasons, {
|
||
fields: [seasonParticipantResults.sportsSeasonId],
|
||
references: [sportsSeasons.id],
|
||
}),
|
||
}));
|
||
|
||
export const teamsRelations = relations(teams, ({ one }) => ({
|
||
season: one(seasons, {
|
||
fields: [teams.seasonId],
|
||
references: [seasons.id],
|
||
}),
|
||
draftSlot: one(draftSlots, {
|
||
fields: [teams.id],
|
||
references: [draftSlots.teamId],
|
||
}),
|
||
}));
|
||
|
||
export const draftSlotsRelations = relations(draftSlots, ({ one }) => ({
|
||
season: one(seasons, {
|
||
fields: [draftSlots.seasonId],
|
||
references: [seasons.id],
|
||
}),
|
||
team: one(teams, {
|
||
fields: [draftSlots.teamId],
|
||
references: [teams.id],
|
||
}),
|
||
}));
|
||
|
||
export const draftPicksRelations = relations(draftPicks, ({ one }) => ({
|
||
season: one(seasons, {
|
||
fields: [draftPicks.seasonId],
|
||
references: [seasons.id],
|
||
}),
|
||
team: one(teams, {
|
||
fields: [draftPicks.teamId],
|
||
references: [teams.id],
|
||
}),
|
||
participant: one(seasonParticipants, {
|
||
fields: [draftPicks.participantId],
|
||
references: [seasonParticipants.id],
|
||
}),
|
||
}));
|
||
|
||
export const draftQueueRelations = relations(draftQueue, ({ one }) => ({
|
||
season: one(seasons, {
|
||
fields: [draftQueue.seasonId],
|
||
references: [seasons.id],
|
||
}),
|
||
team: one(teams, {
|
||
fields: [draftQueue.teamId],
|
||
references: [teams.id],
|
||
}),
|
||
participant: one(seasonParticipants, {
|
||
fields: [draftQueue.participantId],
|
||
references: [seasonParticipants.id],
|
||
}),
|
||
}));
|
||
|
||
export const watchlistRelations = relations(watchlist, ({ one }) => ({
|
||
season: one(seasons, {
|
||
fields: [watchlist.seasonId],
|
||
references: [seasons.id],
|
||
}),
|
||
team: one(teams, {
|
||
fields: [watchlist.teamId],
|
||
references: [teams.id],
|
||
}),
|
||
participant: one(seasonParticipants, {
|
||
fields: [watchlist.participantId],
|
||
references: [seasonParticipants.id],
|
||
}),
|
||
}));
|
||
|
||
export const autodraftSettingsRelations = relations(autodraftSettings, ({ one }) => ({
|
||
season: one(seasons, {
|
||
fields: [autodraftSettings.seasonId],
|
||
references: [seasons.id],
|
||
}),
|
||
team: one(teams, {
|
||
fields: [autodraftSettings.teamId],
|
||
references: [teams.id],
|
||
}),
|
||
}));
|
||
|
||
// Scoring System Relations
|
||
|
||
export const scoringEventsRelations = relations(scoringEvents, ({ one, many }) => ({
|
||
sportsSeason: one(sportsSeasons, {
|
||
fields: [scoringEvents.sportsSeasonId],
|
||
references: [sportsSeasons.id],
|
||
}),
|
||
tournament: one(tournaments, {
|
||
fields: [scoringEvents.tournamentId],
|
||
references: [tournaments.id],
|
||
}),
|
||
eventResults: many(eventResults),
|
||
playoffMatches: many(playoffMatches),
|
||
tournamentGroups: many(tournamentGroups),
|
||
cs2MajorStageResults: many(cs2MajorStageResults),
|
||
}));
|
||
|
||
export const eventResultsRelations = relations(eventResults, ({ one }) => ({
|
||
scoringEvent: one(scoringEvents, {
|
||
fields: [eventResults.scoringEventId],
|
||
references: [scoringEvents.id],
|
||
}),
|
||
seasonParticipant: one(seasonParticipants, {
|
||
fields: [eventResults.seasonParticipantId],
|
||
references: [seasonParticipants.id],
|
||
}),
|
||
}));
|
||
|
||
export const playoffMatchesRelations = relations(playoffMatches, ({ one, many }) => ({
|
||
scoringEvent: one(scoringEvents, {
|
||
fields: [playoffMatches.scoringEventId],
|
||
references: [scoringEvents.id],
|
||
}),
|
||
participant1: one(seasonParticipants, {
|
||
fields: [playoffMatches.participant1Id],
|
||
references: [seasonParticipants.id],
|
||
}),
|
||
participant2: one(seasonParticipants, {
|
||
fields: [playoffMatches.participant2Id],
|
||
references: [seasonParticipants.id],
|
||
}),
|
||
winner: one(seasonParticipants, {
|
||
fields: [playoffMatches.winnerId],
|
||
references: [seasonParticipants.id],
|
||
}),
|
||
loser: one(seasonParticipants, {
|
||
fields: [playoffMatches.loserId],
|
||
references: [seasonParticipants.id],
|
||
}),
|
||
games: many(playoffMatchGames),
|
||
odds: many(playoffMatchOdds),
|
||
}));
|
||
|
||
export const playoffMatchGamesRelations = relations(playoffMatchGames, ({ one }) => ({
|
||
match: one(playoffMatches, {
|
||
fields: [playoffMatchGames.playoffMatchId],
|
||
references: [playoffMatches.id],
|
||
}),
|
||
winner: one(seasonParticipants, {
|
||
fields: [playoffMatchGames.winnerId],
|
||
references: [seasonParticipants.id],
|
||
}),
|
||
}));
|
||
|
||
export const playoffMatchOddsRelations = relations(playoffMatchOdds, ({ one }) => ({
|
||
match: one(playoffMatches, {
|
||
fields: [playoffMatchOdds.playoffMatchId],
|
||
references: [playoffMatches.id],
|
||
}),
|
||
participant: one(seasonParticipants, {
|
||
fields: [playoffMatchOdds.participantId],
|
||
references: [seasonParticipants.id],
|
||
}),
|
||
}));
|
||
|
||
export const seasonParticipantQualifyingTotalsRelations = relations(seasonParticipantQualifyingTotals, ({ one }) => ({
|
||
participant: one(seasonParticipants, {
|
||
fields: [seasonParticipantQualifyingTotals.participantId],
|
||
references: [seasonParticipants.id],
|
||
}),
|
||
sportsSeason: one(sportsSeasons, {
|
||
fields: [seasonParticipantQualifyingTotals.sportsSeasonId],
|
||
references: [sportsSeasons.id],
|
||
}),
|
||
}));
|
||
|
||
export const qualifyingPointConfigRelations = relations(qualifyingPointConfig, ({ one }) => ({
|
||
sportsSeason: one(sportsSeasons, {
|
||
fields: [qualifyingPointConfig.sportsSeasonId],
|
||
references: [sportsSeasons.id],
|
||
}),
|
||
}));
|
||
|
||
export const teamSportScoresRelations = relations(teamSportScores, ({ one }) => ({
|
||
team: one(teams, {
|
||
fields: [teamSportScores.teamId],
|
||
references: [teams.id],
|
||
}),
|
||
sportsSeason: one(sportsSeasons, {
|
||
fields: [teamSportScores.sportsSeasonId],
|
||
references: [sportsSeasons.id],
|
||
}),
|
||
}));
|
||
|
||
export const teamStandingsRelations = relations(teamStandings, ({ one }) => ({
|
||
team: one(teams, {
|
||
fields: [teamStandings.teamId],
|
||
references: [teams.id],
|
||
}),
|
||
season: one(seasons, {
|
||
fields: [teamStandings.seasonId],
|
||
references: [seasons.id],
|
||
}),
|
||
}));
|
||
|
||
export const teamStandingsSnapshotsRelations = relations(teamStandingsSnapshots, ({ one }) => ({
|
||
team: one(teams, {
|
||
fields: [teamStandingsSnapshots.teamId],
|
||
references: [teams.id],
|
||
}),
|
||
season: one(seasons, {
|
||
fields: [teamStandingsSnapshots.seasonId],
|
||
references: [seasons.id],
|
||
}),
|
||
}));
|
||
|
||
export const seasonParticipantExpectedValuesRelations = relations(seasonParticipantExpectedValues, ({ one }) => ({
|
||
participant: one(seasonParticipants, {
|
||
fields: [seasonParticipantExpectedValues.participantId],
|
||
references: [seasonParticipants.id],
|
||
}),
|
||
sportsSeason: one(sportsSeasons, {
|
||
fields: [seasonParticipantExpectedValues.sportsSeasonId],
|
||
references: [sportsSeasons.id],
|
||
}),
|
||
}));
|
||
|
||
export const participantSeasonResultsRelations = relations(participantSeasonResults, ({ one }) => ({
|
||
participant: one(seasonParticipants, {
|
||
fields: [participantSeasonResults.participantId],
|
||
references: [seasonParticipants.id],
|
||
}),
|
||
sportsSeason: one(sportsSeasons, {
|
||
fields: [participantSeasonResults.sportsSeasonId],
|
||
references: [sportsSeasons.id],
|
||
}),
|
||
}));
|
||
|
||
// Tournament Group Relations
|
||
export const tournamentGroupsRelations = relations(tournamentGroups, ({ one, many }) => ({
|
||
scoringEvent: one(scoringEvents, {
|
||
fields: [tournamentGroups.scoringEventId],
|
||
references: [scoringEvents.id],
|
||
}),
|
||
members: many(tournamentGroupMembers),
|
||
matches: many(groupStageMatches),
|
||
}));
|
||
|
||
export const tournamentGroupMembersRelations = relations(tournamentGroupMembers, ({ one }) => ({
|
||
group: one(tournamentGroups, {
|
||
fields: [tournamentGroupMembers.tournamentGroupId],
|
||
references: [tournamentGroups.id],
|
||
}),
|
||
participant: one(seasonParticipants, {
|
||
fields: [tournamentGroupMembers.participantId],
|
||
references: [seasonParticipants.id],
|
||
}),
|
||
}));
|
||
|
||
export const groupStageMatchesRelations = relations(groupStageMatches, ({ one }) => ({
|
||
group: one(tournamentGroups, {
|
||
fields: [groupStageMatches.tournamentGroupId],
|
||
references: [tournamentGroups.id],
|
||
}),
|
||
participant1: one(seasonParticipants, {
|
||
fields: [groupStageMatches.participant1Id],
|
||
references: [seasonParticipants.id],
|
||
relationName: "groupMatchParticipant1",
|
||
}),
|
||
participant2: one(seasonParticipants, {
|
||
fields: [groupStageMatches.participant2Id],
|
||
references: [seasonParticipants.id],
|
||
relationName: "groupMatchParticipant2",
|
||
}),
|
||
}));
|
||
|
||
export const participantEvSnapshotsRelations = relations(participantEvSnapshots, ({ one }) => ({
|
||
participant: one(seasonParticipants, {
|
||
fields: [participantEvSnapshots.participantId],
|
||
references: [seasonParticipants.id],
|
||
}),
|
||
sportsSeason: one(sportsSeasons, {
|
||
fields: [participantEvSnapshots.sportsSeasonId],
|
||
references: [sportsSeasons.id],
|
||
}),
|
||
}));
|
||
|
||
export const teamEvSnapshotsRelations = relations(teamEvSnapshots, ({ one }) => ({
|
||
team: one(teams, {
|
||
fields: [teamEvSnapshots.teamId],
|
||
references: [teams.id],
|
||
}),
|
||
season: one(seasons, {
|
||
fields: [teamEvSnapshots.seasonId],
|
||
references: [seasons.id],
|
||
}),
|
||
}));
|
||
|
||
// Regular season W/L standings for team-sport playoff_bracket seasons (NBA, NHL, etc.)
|
||
// Populated by the standings sync service; syncedAt = null means manually entered
|
||
export const regularSeasonStandings = pgTable("regular_season_standings", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
participantId: uuid("participant_id")
|
||
.notNull()
|
||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||
sportsSeasonId: uuid("sports_season_id")
|
||
.notNull()
|
||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||
wins: integer("wins").notNull().default(0),
|
||
losses: integer("losses").notNull().default(0),
|
||
otLosses: integer("ot_losses"), // NHL overtime losses; null for NBA
|
||
ties: integer("ties"), // draws/ties (e.g. soccer, AFL)
|
||
tablePoints: integer("table_points"), // league table points (e.g. EPL: 3W + D, including deductions)
|
||
goalsFor: integer("goals_for"),
|
||
goalsAgainst: integer("goals_against"),
|
||
goalDifference: integer("goal_difference"),
|
||
winPct: decimal("win_pct", { precision: 5, scale: 4 }),
|
||
gamesPlayed: integer("games_played").notNull().default(0),
|
||
gamesBack: decimal("games_back", { precision: 5, scale: 1 }),
|
||
conference: varchar("conference", { length: 100 }),
|
||
division: varchar("division", { length: 100 }),
|
||
conferenceRank: integer("conference_rank"),
|
||
divisionRank: integer("division_rank"),
|
||
leagueRank: integer("league_rank"),
|
||
streak: varchar("streak", { length: 20 }), // e.g. "W3", "L2"
|
||
lastTen: varchar("last_ten", { length: 15 }), // e.g. "7-2-1"
|
||
homeRecord: varchar("home_record", { length: 15 }),
|
||
awayRecord: varchar("away_record", { length: 15 }),
|
||
externalTeamId: varchar("external_team_id", { length: 255 }),
|
||
srs: decimal("srs", { precision: 6, scale: 2 }), // Simple Rating System (point diff adjusted for SOS)
|
||
syncedAt: timestamp("synced_at"), // null = manually entered
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
}, (table) => ({
|
||
uniqueParticipantSeason: uniqueIndex("rss_participant_season_idx")
|
||
.on(table.participantId, table.sportsSeasonId),
|
||
}));
|
||
|
||
export const regularSeasonStandingsRelations = relations(regularSeasonStandings, ({ one }) => ({
|
||
participant: one(seasonParticipants, {
|
||
fields: [regularSeasonStandings.participantId],
|
||
references: [seasonParticipants.id],
|
||
}),
|
||
sportsSeason: one(sportsSeasons, {
|
||
fields: [regularSeasonStandings.sportsSeasonId],
|
||
references: [sportsSeasons.id],
|
||
}),
|
||
}));
|
||
|
||
// Unmatched teams from a standings sync — awaiting admin resolution.
|
||
// Once the admin maps an external team to a participant, this record is deleted
|
||
// and the standing + participant.externalId are written.
|
||
export const pendingStandingsMappings = pgTable("pending_standings_mappings", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
sportsSeasonId: uuid("sports_season_id")
|
||
.notNull()
|
||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||
externalTeamId: varchar("external_team_id", { length: 255 }).notNull(),
|
||
teamName: varchar("team_name", { length: 255 }).notNull(),
|
||
// Full standing data from the API, stored as JSON for use when resolving
|
||
standingData: jsonb("standing_data").notNull(),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
}, (table) => ({
|
||
uniqueSeasonExternalId: uniqueIndex("psm_season_external_id_idx")
|
||
.on(table.sportsSeasonId, table.externalTeamId),
|
||
}));
|
||
|
||
export const pendingStandingsMappingsRelations = relations(pendingStandingsMappings, ({ one }) => ({
|
||
sportsSeason: one(sportsSeasons, {
|
||
fields: [pendingStandingsMappings.sportsSeasonId],
|
||
references: [sportsSeasons.id],
|
||
}),
|
||
}));
|
||
|
||
// Surface Elos are now stored canonically in `participant_surface_elos`
|
||
// (defined above). The per-window `season_participant_surface_elos` table
|
||
// was dropped in Phase 4 of the canonical tournament layer migration.
|
||
|
||
// ─── Participant Golf Skills (Canonical) ──────────────────────────────────────
|
||
// Canonical golf skill data: one row per real-world player, shared across all
|
||
// sports season windows (matching the participant_surface_elos pattern).
|
||
//
|
||
// Primary metric: SG: Total (strokes gained per round vs. field average).
|
||
// Optional per-major American odds allow major-specific probability blending.
|
||
//
|
||
// The per-window `season_participant_golf_skills` table (previously named
|
||
// `participant_golf_skills`) was the old per-season version. It will be dropped
|
||
// after this migration is confirmed working.
|
||
// TODO: Drop `season_participant_golf_skills` table and remove its schema +
|
||
// relations once the canonical migration is confirmed working.
|
||
|
||
export const participantGolfSkills = pgTable("participant_golf_skills", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
participantId: uuid("participant_id")
|
||
.notNull()
|
||
.references(() => participants.id, { onDelete: "cascade" }),
|
||
sgTotal: decimal("sg_total", { precision: 5, scale: 2 }),
|
||
datagolfRank: integer("datagolf_rank"),
|
||
mastersOdds: integer("masters_odds"),
|
||
usOpenOdds: integer("us_open_odds"),
|
||
openChampionshipOdds: integer("open_championship_odds"),
|
||
pgaChampionshipOdds: integer("pga_championship_odds"),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
}, (t) => ({
|
||
uniqueParticipant: uniqueIndex("participant_golf_skills_participant_unique")
|
||
.on(t.participantId),
|
||
}));
|
||
|
||
export const participantGolfSkillsRelations = relations(participantGolfSkills, ({ one }) => ({
|
||
participant: one(participants, {
|
||
fields: [participantGolfSkills.participantId],
|
||
references: [participants.id],
|
||
}),
|
||
}));
|
||
|
||
export const seasonParticipantGolfSkills = pgTable("season_participant_golf_skills", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
participantId: uuid("participant_id")
|
||
.notNull()
|
||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||
sportsSeasonId: uuid("sports_season_id")
|
||
.notNull()
|
||
.references(() => sportsSeasons.id, { onDelete: "cascade" }),
|
||
sgTotal: decimal("sg_total", { precision: 5, scale: 2 }),
|
||
datagolfRank: integer("datagolf_rank"),
|
||
mastersOdds: integer("masters_odds"),
|
||
usOpenOdds: integer("us_open_odds"),
|
||
openChampionshipOdds: integer("open_championship_odds"),
|
||
pgaChampionshipOdds: integer("pga_championship_odds"),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
}, (t) => ({
|
||
uniqueParticipantSeason: uniqueIndex("season_participant_golf_skills_unique")
|
||
.on(t.participantId, t.sportsSeasonId),
|
||
}));
|
||
|
||
export const seasonParticipantGolfSkillsRelations = relations(seasonParticipantGolfSkills, ({ one }) => ({
|
||
participant: one(seasonParticipants, {
|
||
fields: [seasonParticipantGolfSkills.participantId],
|
||
references: [seasonParticipants.id],
|
||
}),
|
||
sportsSeason: one(sportsSeasons, {
|
||
fields: [seasonParticipantGolfSkills.sportsSeasonId],
|
||
references: [sportsSeasons.id],
|
||
}),
|
||
}));
|
||
|
||
// ─── CS2 Major Stage Results ───────────────────────────────────────────────────
|
||
// Tracks which teams are assigned to each Swiss stage of a CS2 Major, and their
|
||
// advancement/elimination results. One row per (scoringEventId, participantId).
|
||
//
|
||
// stageEntry: 1=Opening Stage, 2=Elimination Stage, 3=Decider Stage
|
||
// stageEliminated: which stage they were eliminated at (null = made Champions Stage)
|
||
// stageEliminatedWins: wins at time of elimination (0, 1, or 2); used to rank teams
|
||
// within placements 9–16 for Stage 3 QP (2-3 > 1-3 > 0-3)
|
||
// finalPlacement: overall placement 1–32 (set after event completes)
|
||
|
||
export const cs2MajorStageResults = pgTable("cs2_major_stage_results", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
scoringEventId: uuid("scoring_event_id")
|
||
.notNull()
|
||
.references(() => scoringEvents.id, { onDelete: "cascade" }),
|
||
participantId: uuid("participant_id")
|
||
.notNull()
|
||
.references(() => seasonParticipants.id, { onDelete: "cascade" }),
|
||
stageEntry: integer("stage_entry").notNull(),
|
||
stageEliminated: integer("stage_eliminated"),
|
||
stageEliminatedWins: integer("stage_eliminated_wins"),
|
||
finalPlacement: integer("final_placement"),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||
}, (t) => ({
|
||
uniqueEventParticipant: uniqueIndex("cs2_major_stage_results_unique")
|
||
.on(t.scoringEventId, t.participantId),
|
||
}));
|
||
|
||
export const cs2MajorStageResultsRelations = relations(cs2MajorStageResults, ({ one }) => ({
|
||
scoringEvent: one(scoringEvents, {
|
||
fields: [cs2MajorStageResults.scoringEventId],
|
||
references: [scoringEvents.id],
|
||
}),
|
||
participant: one(seasonParticipants, {
|
||
fields: [cs2MajorStageResults.participantId],
|
||
references: [seasonParticipants.id],
|
||
}),
|
||
}));
|
||
|
||
// ─── Commissioner Audit Log ────────────────────────────────────────────────────
|
||
// Immutable log of significant commissioner/admin actions per season.
|
||
// Readable by all league members for transparency; writable only by the
|
||
// server-side route actions that perform the underlying operations.
|
||
|
||
export const auditActionEnum = pgEnum("audit_action", [
|
||
"league_settings_changed",
|
||
"draft_settings_changed",
|
||
"scoring_rules_changed",
|
||
"sports_changed",
|
||
"draft_order_set",
|
||
"draft_order_randomized",
|
||
"draft_started",
|
||
"draft_paused",
|
||
"draft_resumed",
|
||
"draft_reset",
|
||
"draft_rollback",
|
||
"force_autopick",
|
||
"force_manual_pick",
|
||
"draft_pick_changed",
|
||
"time_bank_edited",
|
||
"brackt_resolved",
|
||
]);
|
||
|
||
export const commissionerAuditLog = pgTable("commissioner_audit_log", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
seasonId: uuid("season_id")
|
||
.notNull()
|
||
.references(() => seasons.id, { onDelete: "cascade" }),
|
||
leagueId: uuid("league_id")
|
||
.notNull()
|
||
.references(() => leagues.id, { onDelete: "cascade" }),
|
||
actorUserId: varchar("actor_user_id", { length: 255 }).notNull(),
|
||
actorDisplayName: varchar("actor_display_name", { length: 255 }),
|
||
action: auditActionEnum("action").notNull(),
|
||
affectedTeamIds: text("affected_team_ids").array(),
|
||
details: jsonb("details"),
|
||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||
}, (t) => ({
|
||
auditLogSeasonIdx: index("audit_log_season_id_idx").on(t.seasonId, t.createdAt),
|
||
}));
|
||
|
||
export const commissionerAuditLogRelations = relations(commissionerAuditLog, ({ one }) => ({
|
||
season: one(seasons, {
|
||
fields: [commissionerAuditLog.seasonId],
|
||
references: [seasons.id],
|
||
}),
|
||
league: one(leagues, {
|
||
fields: [commissionerAuditLog.leagueId],
|
||
references: [leagues.id],
|
||
}),
|
||
}));
|
||
|
||
// ─── Team Score Events ─────────────────────────────────────────────────────────
|
||
// Records each time a team's total fantasy points increase as a result of a
|
||
// scoring event completing. Used for the "Recent Scores" feed on the standings
|
||
// page. One row per team per match (bracket sports) or per event (fallback).
|
||
// Participant IDs are stored at write time so attribution is accurate regardless
|
||
// of future match results.
|
||
|
||
export const teamScoreEvents = pgTable("team_score_events", {
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
teamId: uuid("team_id")
|
||
.notNull()
|
||
.references(() => teams.id, { onDelete: "cascade" }),
|
||
seasonId: uuid("season_id")
|
||
.notNull()
|
||
.references(() => seasons.id, { onDelete: "cascade" }),
|
||
scoringEventId: uuid("scoring_event_id")
|
||
.references(() => scoringEvents.id, { onDelete: "set null" }),
|
||
scoringEventName: varchar("scoring_event_name", { length: 255 }),
|
||
sportName: varchar("sport_name", { length: 255 }),
|
||
// Set for bracket sports (one row per match); NULL for event-level fallback rows.
|
||
matchId: uuid("match_id").references(() => playoffMatches.id, { onDelete: "set null" }),
|
||
// Participant IDs captured at write time — the specific drafted participants
|
||
// whose win/advancement caused this score change.
|
||
participantIds: text("participant_ids").array(),
|
||
pointsDelta: decimal("points_delta", { precision: 10, scale: 2 }).notNull(),
|
||
occurredAt: timestamp("occurred_at").defaultNow().notNull(),
|
||
}, (t) => ({
|
||
// Per-match rows: one row per (team, season, match)
|
||
uniqueTeamSeasonMatch: uniqueIndex("team_score_events_match_unique")
|
||
.on(t.teamId, t.seasonId, t.matchId)
|
||
.where(sql`match_id IS NOT NULL`),
|
||
// Event-level fallback rows: one row per (team, season, event) when no match data
|
||
uniqueTeamSeasonEvent: uniqueIndex("team_score_events_event_unique")
|
||
.on(t.teamId, t.seasonId, t.scoringEventId)
|
||
.where(sql`match_id IS NULL`),
|
||
}));
|
||
|
||
export const teamScoreEventsRelations = relations(teamScoreEvents, ({ one }) => ({
|
||
team: one(teams, {
|
||
fields: [teamScoreEvents.teamId],
|
||
references: [teams.id],
|
||
}),
|
||
season: one(seasons, {
|
||
fields: [teamScoreEvents.seasonId],
|
||
references: [seasons.id],
|
||
}),
|
||
scoringEvent: one(scoringEvents, {
|
||
fields: [teamScoreEvents.scoringEventId],
|
||
references: [scoringEvents.id],
|
||
}),
|
||
}));
|