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 firstName: varchar("first_name", { length: 255 }), lastName: varchar("last_name", { length: 255 }), imageUrl: varchar("image_url", { length: 512 }), // mapped as BetterAuth's "image" field isAdmin: boolean("is_admin").notNull().default(false), timezone: varchar("timezone", { length: 50 }), // IANA timezone string, e.g. "America/Chicago" 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", "snooker_bracket", "tennis_qualifying_points", "mlb_bracket", "wnba_bracket", "world_cup", "darts_bracket", "cs2_major_qualifying_points", "ncaa_football_bracket", "llws_bracket", ]); 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 }), 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: 255 }), // Filename of icon 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" }), 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 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 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 sportsSeasonTournaments = pgTable( "sports_season_tournaments", { id: uuid("id").primaryKey().defaultRandom(), sportsSeasonId: uuid("sports_season_id") .notNull() .references(() => sportsSeasons.id, { onDelete: "cascade" }), tournamentId: uuid("tournament_id") .notNull() .references(() => tournaments.id, { onDelete: "cascade" }), createdAt: timestamp("created_at").defaultNow().notNull(), }, (t) => ({ uniqueSeasonTournament: uniqueIndex("sports_season_tournaments_unique").on( t.sportsSeasonId, t.tournamentId, ), tournamentIdIdx: index("sports_season_tournaments_tournament_id_idx").on( t.tournamentId, ), }), ); 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], }), participants: many(seasonParticipants), seasonTemplateSports: many(seasonTemplateSports), seasonSports: many(seasonSports), participantResults: many(seasonParticipantResults), regularSeasonStandings: many(regularSeasonStandings), pendingStandingsMappings: many(pendingStandingsMappings), sportsSeasonTournaments: many(sportsSeasonTournaments), })); 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), })); 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), sportsSeasonTournaments: many(sportsSeasonTournaments), })); 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), })); 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 sportsSeasonTournamentsRelations = relations( sportsSeasonTournaments, ({ one }) => ({ sportsSeason: one(sportsSeasons, { fields: [sportsSeasonTournaments.sportsSeasonId], references: [sportsSeasons.id], }), tournament: one(tournaments, { fields: [sportsSeasonTournaments.tournamentId], references: [tournaments.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"), // future sports (e.g. soccer) 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", ]); 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], }), }));