From cf8ac8a765cecc1bb47ab482bc5b67f75f2d03c7 Mon Sep 17 00:00:00 2001 From: Chris Parsons <438676+chrisparsons83@users.noreply.github.com> Date: Tue, 10 Mar 2026 10:27:58 -0700 Subject: [PATCH] feat: progressive floor scoring for playoff brackets (#100) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a participant wins a bracket round, they immediately earn provisional "floor" points (the averaged minimum they'd receive if eliminated next round). These update as they advance and are replaced by finalized scores on elimination. Key changes: - Add `is_partial_score` column to `participant_results` (migration 0038) - `processPlayoffEvent`: assign provisional position 5 to non-scoring round winners; assign round-appropriate floors to scoring round winners via `getGuaranteedMinimumPosition`; add catch-all for unrecognized round names - `upsertParticipantResult`: guard against un-finalizing rows (never overwrite isPartialScore=false with true) - `calculateBracketPoints`: new function averaging tied bracket tiers (5-8 → 20 pts, 3-4 → avg, 1-2 solo); used in `calculateTeamScore` for playoff_bracket pattern (pattern-aware, doesn't affect F1/golf scoring) - `PlayoffBracket`: "In Contention" table for still-active participants; AFL double-chance fix (participants who won a later match excluded from earlier round's loser list); correct `nextRank` starting position - Server loader: batch owner DB queries (one query vs N+1); deduplicate participantPoints; use calculateBracketPoints for bracket point display - Clean up Phase/Q-number tracking comments throughout scoring-calculator.ts - 3 new tests for non-scoring round provisional floor behavior Also includes a dev admin bypass via DEV_ADMIN_CLERK_ID env var (separate change on this branch, not part of floor scoring feature). Co-authored-by: Claude Sonnet 4.6 --- .env.example | 1 + app/components/scoring/PlayoffBracket.tsx | 118 +- app/components/scoring/SportSeasonDisplay.tsx | 3 + .../progressive-floor-scoring.test.ts | 170 + app/models/scoring-calculator.ts | 226 +- app/models/scoring-rules.ts | 28 + app/models/user.ts | 3 + ...d.sports-seasons.$sportsSeasonId.server.ts | 67 +- ...eagueId.sports-seasons.$sportsSeasonId.tsx | 2 + .../__tests__/probability-updater.test.ts | 5 + database/schema.ts | 1 + drizzle/0038_sad_wilson_fisk.sql | 2 + drizzle/meta/0038_snapshot.json | 3303 +++++++++++++++++ drizzle/meta/_journal.json | 7 + 14 files changed, 3866 insertions(+), 70 deletions(-) create mode 100644 app/models/__tests__/progressive-floor-scoring.test.ts create mode 100644 drizzle/0038_sad_wilson_fisk.sql create mode 100644 drizzle/meta/0038_snapshot.json diff --git a/.env.example b/.env.example index d2e999b..2fb83aa 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,4 @@ CLERK_SECRET_KEY="" CLERK_PUBLISHABLE_KEY="" CLERK_WEBHOOK_SECRET="" CONTAINER_REGISTRY="" +DEV_ADMIN_CLERK_ID="" \ No newline at end of file diff --git a/app/components/scoring/PlayoffBracket.tsx b/app/components/scoring/PlayoffBracket.tsx index fc45525..d5c0994 100644 --- a/app/components/scoring/PlayoffBracket.tsx +++ b/app/components/scoring/PlayoffBracket.tsx @@ -45,6 +45,7 @@ interface PlayoffBracketProps { rounds: string[]; // Ordered list of round names (earliest first) preEliminatedParticipants?: { id: string; name: string }[]; // Eliminated before bracket (e.g. group stage) participantPoints?: { participantId: string; points: number }[]; // Computed fantasy points per participant + partialScoreParticipantIds?: string[]; // Still-competing participants with provisional floor scores teamOwnerships?: TeamOwnership[]; userParticipantIds?: string[]; showOwnership?: boolean; @@ -114,6 +115,7 @@ export function PlayoffBracket({ rounds, preEliminatedParticipants = [], participantPoints = [], + partialScoreParticipantIds = [], teamOwnerships = [], userParticipantIds = [], showOwnership = true, @@ -135,7 +137,9 @@ export function PlayoffBracket({ return `Winner of ${feeder.round} M${feeder.matchNumber}`; }; - // Build elimination rankings: collect losers per round, then assign rank labels + // Build elimination rankings: collect losers per round, then assign rank labels. + // In double-chance brackets (e.g. AFL), a participant may lose one round but + // win a later one — only count them as eliminated at their FINAL losing match. const losersByRound = new Map>(); let hasScore = false; let bracketWinner: Participant | null = null; @@ -146,8 +150,17 @@ export function PlayoffBracket({ : null; if (finalMatch?.winner) bracketWinner = finalMatch.winner; + // Build the set of all participants who won at least one completed match. + // If a participant won any match, their earlier loss was not their final elimination. + const participantWinIds = new Set(); + for (const match of matches) { + if (match.isComplete && match.winnerId) participantWinIds.add(match.winnerId); + } + for (const match of matches) { if (!match.isComplete || !match.loser) continue; + // Skip if this loser went on to win another match (double-chance bracket) + if (participantWinIds.has(match.loser.id)) continue; const loserScore = match.loserId === match.participant1Id ? match.participant1Score @@ -162,21 +175,49 @@ export function PlayoffBracket({ } // Walk rounds latest→earliest to assign rank labels (no mutation) + // nextRank must start after the still-alive participants (not always 2) + const allBracketParticipantIds = new Set(); + for (const match of matches) { + if (match.participant1Id) allBracketParticipantIds.add(match.participant1Id); + if (match.participant2Id) allBracketParticipantIds.add(match.participant2Id); + } + const totalEliminatedInBracket = [...losersByRound.values()].reduce( + (sum, losers) => sum + losers.length, + 0 + ); + const stillAlive = + allBracketParticipantIds.size - totalEliminatedInBracket - (bracketWinner ? 1 : 0); const rankedEntries: EliminatedEntry[] = []; - let nextRank = 2; + let nextRank = stillAlive + (bracketWinner ? 2 : 1); for (let ri = rounds.length - 1; ri >= 0; ri--) { const roundLosers = losersByRound.get(rounds[ri]) || []; if (roundLosers.length === 0) continue; - const rankEnd = nextRank + roundLosers.length - 1; - const rankLabel = - nextRank === rankEnd ? `#${nextRank}` : `#${nextRank}–${rankEnd}`; + const rankLabel = `T${nextRank}`; for (const loser of roundLosers) { rankedEntries.push({ ...loser, rankLabel }); } nextRank += roundLosers.length; } - const showRankings = rankedEntries.length > 0 || bracketWinner !== null || preEliminatedParticipants.length > 0; + // Exclude pre-eliminated participants already ranked via bracket match losers + const rankedParticipantIds = new Set(rankedEntries.map((e) => e.participant.id)); + if (bracketWinner) rankedParticipantIds.add(bracketWinner.id); + const filteredPreEliminated = preEliminatedParticipants.filter( + (p) => !rankedParticipantIds.has(p.id) + ); + + const showRankings = rankedEntries.length > 0 || bracketWinner !== null || filteredPreEliminated.length > 0; + + // Build participant lookup from match data for the "In Contention" table + const participantMap = new Map(); + for (const match of matches) { + if (match.participant1) participantMap.set(match.participant1.id, match.participant1); + if (match.participant2) participantMap.set(match.participant2.id, match.participant2); + } + const activeParticipants = [...allBracketParticipantIds] + .filter((id) => !rankedParticipantIds.has(id)) + .map((id) => participantMap.get(id)) + .filter((p): p is Participant => p !== undefined); // Hoist winner row lookups so we don't need an IIFE in JSX const winnerIsOwned = bracketWinner ? userParticipantSet.has(bracketWinner.id) : false; @@ -370,7 +411,68 @@ export function PlayoffBracket({ ); })} - {/* Final Rankings */} + {/* In Contention */} + {activeParticipants.length > 0 && ( + + + + + In Contention + + + + + + + Participant + {showOwnership && ( + Drafted By + )} + {pointsMap.size > 0 && ( + Pts + )} + + + + {activeParticipants.map((p) => { + const isOwned = userParticipantSet.has(p.id); + const ownership = showOwnership ? ownershipMap.get(p.id) || null : null; + const pts = pointsMap.get(p.id); + return ( + + + {p.name} + {isOwned && ( + + )} + + {showOwnership && ( + + {ownership ? ( + + ) : ( + - + )} + + )} + {pointsMap.size > 0 && ( + + {pts !== undefined ? pts : "–"} + + )} + + ); + })} + +
+
+
+ )} + + {/* Final Rankings / Eliminated Teams */} {showRankings && ( @@ -463,7 +565,7 @@ export function PlayoffBracket({ ); })} - {preEliminatedParticipants.map((p) => { + {filteredPreEliminated.map((p) => { const isOwned = userParticipantSet.has(p.id); const ownership = showOwnership ? ownershipMap.get(p.id) || null : null; const pts = pointsMap.get(p.id); diff --git a/app/components/scoring/SportSeasonDisplay.tsx b/app/components/scoring/SportSeasonDisplay.tsx index 451c861..2cc4df5 100644 --- a/app/components/scoring/SportSeasonDisplay.tsx +++ b/app/components/scoring/SportSeasonDisplay.tsx @@ -98,6 +98,7 @@ interface SportSeasonDisplayProps { playoffRounds?: string[]; preEliminatedParticipants?: { id: string; name: string }[]; participantPoints?: { participantId: string; points: number }[]; + partialScoreParticipantIds?: string[]; // Season standings data (F1) seasonStandings?: SeasonStanding[]; @@ -125,6 +126,7 @@ export function SportSeasonDisplay({ playoffRounds = [], preEliminatedParticipants = [], participantPoints = [], + partialScoreParticipantIds = [], seasonStandings = [], seasonIsFinalized = false, qpStandings = [], @@ -147,6 +149,7 @@ export function SportSeasonDisplay({ rounds={playoffRounds} preEliminatedParticipants={preEliminatedParticipants} participantPoints={participantPoints} + partialScoreParticipantIds={partialScoreParticipantIds} teamOwnerships={teamOwnerships} userParticipantIds={userParticipantIds} showOwnership={showOwnership} diff --git a/app/models/__tests__/progressive-floor-scoring.test.ts b/app/models/__tests__/progressive-floor-scoring.test.ts new file mode 100644 index 0000000..d62068f --- /dev/null +++ b/app/models/__tests__/progressive-floor-scoring.test.ts @@ -0,0 +1,170 @@ +import { describe, it, expect } from "vitest"; +import { getGuaranteedMinimumPosition } from "../scoring-calculator"; +import { calculateAveragedPoints, calculateBracketPoints, calculateFantasyPoints, type ScoringRules } from "../scoring-rules"; + +const DEFAULT_SCORING: ScoringRules = { + pointsFor1st: 100, + pointsFor2nd: 70, + pointsFor3rd: 50, + pointsFor4th: 40, + pointsFor5th: 25, + pointsFor6th: 25, + pointsFor7th: 15, + pointsFor8th: 15, +}; + +describe("Progressive Floor Scoring", () => { + describe("getGuaranteedMinimumPosition", () => { + describe("Standard bracket rounds", () => { + it("returns null for Finals — winner is finalized as 1st", () => { + expect(getGuaranteedMinimumPosition("Finals", null, true)).toBeNull(); + expect(getGuaranteedMinimumPosition("Championship", null, true)).toBeNull(); + expect(getGuaranteedMinimumPosition("Super Bowl", null, true)).toBeNull(); + expect(getGuaranteedMinimumPosition("NBA Finals", null, true)).toBeNull(); + expect(getGuaranteedMinimumPosition("Grand Final", "afl_10", true)).toBeNull(); + }); + + it("returns 2 for Semifinals — winners go to Final, guaranteed at worst 2nd", () => { + expect(getGuaranteedMinimumPosition("Semifinals", null, true)).toBe(2); + expect(getGuaranteedMinimumPosition("Final Four", null, true)).toBe(2); + expect(getGuaranteedMinimumPosition("Conference Finals", null, true)).toBe(2); + expect(getGuaranteedMinimumPosition("Conference Championship", null, true)).toBe(2); + }); + + it("returns 3 for Quarterfinals — winners go to Semis, guaranteed at worst 3rd-4th", () => { + expect(getGuaranteedMinimumPosition("Quarterfinals", null, true)).toBe(3); + expect(getGuaranteedMinimumPosition("Elite Eight", null, true)).toBe(3); + expect(getGuaranteedMinimumPosition("Divisional", null, true)).toBe(3); + expect(getGuaranteedMinimumPosition("Conference Semifinals", null, true)).toBe(3); + }); + + it("returns 5 for earlier scoring rounds — winners are guaranteed top-8", () => { + expect(getGuaranteedMinimumPosition("Round of 16", null, true)).toBe(5); + expect(getGuaranteedMinimumPosition("Round of 32", null, true)).toBe(5); + expect(getGuaranteedMinimumPosition("Wild Card Round", null, true)).toBe(5); + }); + + it("returns null for non-scoring rounds — winning doesn't guarantee points yet", () => { + expect(getGuaranteedMinimumPosition("Round of 16", null, false)).toBeNull(); + expect(getGuaranteedMinimumPosition("First Four", null, false)).toBeNull(); + expect(getGuaranteedMinimumPosition("Quarterfinals", null, false)).toBeNull(); + }); + }); + + describe("AFL bracket (afl_10)", () => { + it("returns 3 for Qualifying Finals winners — advance to Preliminary Finals", () => { + expect(getGuaranteedMinimumPosition("Qualifying Finals", "afl_10", true)).toBe(3); + }); + + it("returns 5 for Elimination Finals winners — advance to Semi-Finals", () => { + expect(getGuaranteedMinimumPosition("Elimination Finals", "afl_10", true)).toBe(5); + }); + + it("returns 3 for AFL Semi-Finals winners — advance to Preliminary Finals", () => { + expect(getGuaranteedMinimumPosition("Semi-Finals", "afl_10", true)).toBe(3); + }); + + it("returns 2 for Preliminary Finals winners — advance to Grand Final", () => { + expect(getGuaranteedMinimumPosition("Preliminary Finals", "afl_10", true)).toBe(2); + }); + }); + }); + + describe("Progressive point values (guaranteed minimum points)", () => { + it("standard bracket: 8-team path to champion earns correct floor at each stage", () => { + // Win Quarterfinals → guaranteed at worst 3rd-4th + const afterQF = calculateAveragedPoints([3, 4], DEFAULT_SCORING); + expect(afterQF).toBe(45); // (50 + 40) / 2 + + // Win Semifinals → guaranteed at worst 2nd + const afterSF = calculateFantasyPoints(2, DEFAULT_SCORING); + expect(afterSF).toBe(70); + + // Win Finals → 1st place + const champion = calculateFantasyPoints(1, DEFAULT_SCORING); + expect(champion).toBe(100); + }); + + it("standard bracket: losing in Round of 16 earns 0 pts (non-scoring)", () => { + // Non-scoring round losers get eliminated with 0 pts + expect(calculateFantasyPoints(0, DEFAULT_SCORING)).toBe(0); + }); + + it("standard bracket: Quarterfinal survivors start at 20 pts floor (avg of 5-8)", () => { + // 8 winners from R16 advance to QF. If all 8 lose QF → tied 5th-8th → 20 pts each. + // But only 4 lose QF; the 4 winners advance. Their guaranteed minimum = avg(3, 4). + // The R16 winners' guaranteed minimum (before QF is played) would be avg(5,6,7,8) = 20. + const r16WinnersFloor = calculateAveragedPoints([5, 6, 7, 8], DEFAULT_SCORING); + expect(r16WinnersFloor).toBe(20); // (25+25+15+15)/4 + }); + + it("AFL: Elimination Finals winners get 5th-6th floor (avg)", () => { + // EF winners advance to Semi-Finals. If they lose Semi-Finals → 5th-6th. + const efWinnersFloor = calculateAveragedPoints([5, 6], DEFAULT_SCORING); + expect(efWinnersFloor).toBe(25); // (25+25)/2 = 25 + }); + + it("AFL: Qualifying Finals winners get 3rd-4th floor (avg)", () => { + // QF winners advance to Preliminary Finals. If they lose → 3rd-4th. + const qfWinnersFloor = calculateAveragedPoints([3, 4], DEFAULT_SCORING); + expect(qfWinnersFloor).toBe(45); // (50+40)/2 + }); + + it("AFL: Qualifying Finals losers (second chance) get 5th-6th floor", () => { + // QF losers drop to Semi-Finals. If they lose Semi-Finals → 5th-6th. + const qfLosersFloor = calculateAveragedPoints([5, 6], DEFAULT_SCORING); + expect(qfLosersFloor).toBe(25); // (25+25)/2 = 25 + }); + }); + + describe("Non-scoring round provisional floors", () => { + it("R16 winners (non-scoring) get T5 floor = 20 pts", () => { + // After winning a non-scoring R16, participants are guaranteed top-8. + // processPlayoffEvent assigns finalPosition=5 with isPartialScore=true. + // calculateBracketPoints maps position 5 → avg(5-8) = 20 pts. + expect(calculateBracketPoints(5, DEFAULT_SCORING)).toBe(20); + }); + + it("non-scoring floor (pos 5) is less than QF winner floor (pos 3)", () => { + // Advancing past QF increases the floor from 20 → 45 pts. + const nonScoringFloor = calculateBracketPoints(5, DEFAULT_SCORING); + const afterQFFloor = calculateBracketPoints(3, DEFAULT_SCORING); + expect(afterQFFloor).toBeGreaterThan(nonScoringFloor); + }); + + it("position 5 stored for non-scoring winner gives correct 20 pts via calculateBracketPoints", () => { + // The non-scoring branch stores finalPosition=5 (fixed). + // calculateBracketPoints correctly averages [5,6,7,8] regardless of how many + // winners the non-scoring round produced. + expect(calculateBracketPoints(5, DEFAULT_SCORING)).toBe(20); // (25+25+15+15)/4 + }); + }); + + describe("Progressive scoring progression", () => { + it("score increases as participant advances through bracket", () => { + // A participant's guaranteed minimum only increases, never decreases. + // After each round, verify new floor > previous floor. + const afterR16Floor = calculateAveragedPoints([5, 6, 7, 8], DEFAULT_SCORING); // 20 + const afterQFFloor = calculateAveragedPoints([3, 4], DEFAULT_SCORING); // 45 + const afterSFFloor = calculateFantasyPoints(2, DEFAULT_SCORING); // 70 + const champion = calculateFantasyPoints(1, DEFAULT_SCORING); // 100 + + expect(afterQFFloor).toBeGreaterThan(afterR16Floor); + expect(afterSFFloor).toBeGreaterThan(afterQFFloor); + expect(champion).toBeGreaterThan(afterSFFloor); + }); + + it("final winner's partial score (2nd) is replaced by finalized 1st place score", () => { + // Before Finals: finalist has guaranteed minimum of 2nd place + const finalistFloor = calculateFantasyPoints(2, DEFAULT_SCORING); + expect(finalistFloor).toBe(70); + + // After winning Finals: finalized to 1st place + const championScore = calculateFantasyPoints(1, DEFAULT_SCORING); + expect(championScore).toBe(100); + + // Score increased when finalized + expect(championScore).toBeGreaterThan(finalistFloor); + }); + }); +}); diff --git a/app/models/scoring-calculator.ts b/app/models/scoring-calculator.ts index a991076..7d2eaf1 100644 --- a/app/models/scoring-calculator.ts +++ b/app/models/scoring-calculator.ts @@ -1,7 +1,7 @@ import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { eq, and, inArray } from "drizzle-orm"; -import { getScoringRules, calculateFantasyPoints, calculateAveragedPoints } from "./scoring-rules"; +import { getScoringRules, calculateFantasyPoints, calculateAveragedPoints, calculateBracketPoints } from "./scoring-rules"; import { getSeasonResults } from "./participant-season-result"; import { updateProbabilitiesAfterResult } from "~/services/probability-updater"; @@ -16,14 +16,16 @@ export type ScoringPattern = | "qualifying_points"; /** - * Process a playoff event completion and assign final placements + * Process a playoff event completion and assign final placements. * - * Q19: Admin enters the same placement for all tied participants, - * system automatically shares positions + * Loser placement rules per round type: + * Non-scoring rounds: losers get 0 pts (pre-bracket elimination) + * Quarterfinals / equivalent: losers share T5–T8 + * Semifinals / equivalent: losers share T3–T4 + * Finals / equivalent: loser gets 2nd, winner gets 1st * - * Q20: Participants eliminated before Elite Eight get 0 points - * - * Phase 2.7: Updated to support template-based brackets with non-scoring rounds + * Progressive floor scoring: winners immediately earn a provisional placement + * reflecting their worst-case finish from this point forward. */ export async function processPlayoffEvent( eventId: string, @@ -124,21 +126,49 @@ export async function processPlayoffEvent( throw new Error(`Scoring rules not found for season ${seasonSport.seasonId}`); } - // If this is a non-scoring round, award 0 points (Q20) + // Non-scoring rounds: losers are pre-bracket eliminated (0 pts). + // Winners have cleared the qualifier and are guaranteed a top-8 finish, + // so we bank position 5 (T5–T8 floor) as their provisional score immediately. + // Using a fixed value of 5 is correct for standard brackets (R16 winners → QF → T5 worst case) + // and conservatively safe for non-standard cases (never over-promises points). if (!isScoring) { for (const match of matches) { if (match.loserId) { await upsertParticipantResult( match.loserId, event.sportsSeasonId, - 0, // 0 placement = 0 points for early elimination + 0, // 0 = pre-bracket elimination, earns no points db ); } + if (match.winnerId) { + await upsertParticipantResult( + match.winnerId, + event.sportsSeasonId, + 5, // provisional T5–T8 floor: guaranteed top-8 finish + db, + true + ); + } } } - // AFL-specific rounds (Phase 3.3) - else if (round === "Elimination Finals") { + // AFL-specific rounds (afl_10 bracket template) + else if (round === "Qualifying Finals") { + // AFL: Qualifying Finals losers get a second chance via Semi-Finals. + // They are not eliminated here, but earn a guaranteed minimum of 5th-6th. + // Winners are handled by the general guaranteed-minimum loop below (3rd-4th). + for (const match of matches) { + if (match.loserId) { + await upsertParticipantResult( + match.loserId, + event.sportsSeasonId, + 5, // Guaranteed minimum: at worst 5th-6th via Semi-Finals + db, + true // isPartialScore: still competing + ); + } + } + } else if (round === "Elimination Finals") { // AFL: Elimination Finals losers share 7th-8th for (const match of matches) { if (match.loserId) { @@ -171,7 +201,7 @@ export async function processPlayoffEvent( throw new Error("Finals match is not complete"); } - // Update or create participant results for winner + // Update or create participant results for winner (finalized, isPartialScore=false) await upsertParticipantResult( finalMatch.winnerId, event.sportsSeasonId, @@ -179,7 +209,7 @@ export async function processPlayoffEvent( db ); - // Update or create participant results for loser + // Update or create participant results for loser (finalized, isPartialScore=false) await upsertParticipantResult( finalMatch.loserId, event.sportsSeasonId, @@ -210,12 +240,52 @@ export async function processPlayoffEvent( ); } } + } else { + // Unrecognized scoring round: losers are outside the top-8 and earn 0 pts. + // This handles rounds like "Round of 16", "Round of 32" when isScoring=true, + // or any custom round name not explicitly mapped above. + console.warn( + `[ScoringCalculator] Unrecognized scoring round "${round}" for event ${eventId}. Losers receive 0 pts.` + ); + for (const match of matches) { + if (match.loserId) { + await upsertParticipantResult( + match.loserId, + event.sportsSeasonId, + 0, + db + ); + } + } + } + + // Progressive floor scoring: assign guaranteed minimum points to winners. + // Winners of any scoring round (except Finals, which already finalize both + // participants above) earn provisional points reflecting the worst-case + // placement they can achieve from here. These update as they advance. + const guaranteedMinimum = getGuaranteedMinimumPosition( + round, + event.bracketTemplateId, + isScoring + ); + if (guaranteedMinimum !== null) { + for (const match of matches) { + if (match.winnerId) { + await upsertParticipantResult( + match.winnerId, + event.sportsSeasonId, + guaranteedMinimum, + db, + true // isPartialScore: still competing, score will increase as they advance + ); + } + } } // Recalculate standings for all affected leagues await recalculateAffectedLeagues(event.sportsSeasonId, db); - // Auto-trigger probability recalculation (Phase 5.3) + // Auto-trigger probability recalculation after result try { await updateProbabilitiesAfterResult(event.sportsSeasonId, true); console.log( @@ -231,12 +301,17 @@ export async function processPlayoffEvent( /** * Helper to upsert participant result + * + * isPartialScore=true means the participant is still alive and this placement + * is their guaranteed minimum floor — it will be replaced as they advance or + * when they are finally eliminated. */ async function upsertParticipantResult( participantId: string, sportsSeasonId: string, finalPosition: number, - db: ReturnType + db: ReturnType, + isPartialScore = false ): Promise { const existing = await db.query.participantResults.findFirst({ where: and( @@ -246,10 +321,16 @@ async function upsertParticipantResult( }); if (existing) { + // Never un-finalize: if the row is already finalized (isPartialScore=false), + // a call with isPartialScore=true must not overwrite it (e.g. a re-run of + // processPlayoffEvent after a manual finalization). + if (!existing.isPartialScore && isPartialScore) return; + await db .update(schema.participantResults) .set({ finalPosition, + isPartialScore, updatedAt: new Date(), }) .where(eq(schema.participantResults.id, existing.id)); @@ -258,16 +339,78 @@ async function upsertParticipantResult( participantId, sportsSeasonId, finalPosition, + isPartialScore, }); } } /** - * Process a qualifying event completion and update QP totals - * Phase 3.2 Implementation + * Returns the guaranteed minimum final position for winners of a given round. * - * Q4: Manual finalization after all majors complete - * Q5: Ties in QP handled by sharing placements + * This is the worst-case placement a participant can achieve if they lose + * every remaining match from this point forward. Returns null when: + * - The round is non-scoring (winners haven't reached a points-paying zone) + * - The round is the Finals (winners are already being finalized as 1st place) + */ +export function getGuaranteedMinimumPosition( + round: string, + bracketTemplateId: string | null | undefined, + isScoring: boolean +): number | null { + // Non-scoring rounds: winning doesn't guarantee any points yet + if (!isScoring) return null; + + // Finals: winner is finalized as 1st — handled inline, not via this helper + if ( + round === "Finals" || + round === "Championship" || + round === "Super Bowl" || + round === "NBA Finals" || + round === "Grand Final" + ) { + return null; + } + + // AFL-specific routing (afl_10 bracket template) + if (bracketTemplateId === "afl_10") { + // Qualifying Finals: winners → Prelim Finals (losers there get 3rd-4th) + if (round === "Qualifying Finals") return 3; + // Elimination Finals: winners → Semi-Finals (losers there get 5th-6th) + if (round === "Elimination Finals") return 5; + // Semi-Finals: winners → Prelim Finals (losers there get 3rd-4th) + if (round === "Semi-Finals") return 3; + } + + // Standard bracket rounds + // Semi-final winners advance to the Final — guaranteed at worst 2nd place + if ( + round === "Semifinals" || + round === "Final Four" || + round === "Conference Finals" || + round === "Conference Championship" || + round === "Preliminary Finals" + ) { + return 2; + } + + // Quarterfinal winners advance to Semis — guaranteed at worst 3rd-4th + if ( + round === "Quarterfinals" || + round === "Elite Eight" || + round === "Divisional" || + round === "Conference Semifinals" + ) { + return 3; + } + + // All other scoring rounds (e.g. Round of 16, Round of 32, etc.) — + // winners are now guaranteed a top-8 finish at worst (position 5) + return 5; +} + +/** + * Process a qualifying event completion and update QP totals. + * Ties in QP are handled by sharing placements (averaged points). */ export async function processQualifyingEvent( eventId: string, @@ -363,13 +506,9 @@ export async function processQualifyingEvent( } /** - * Finalize qualifying points and convert to fantasy placements - * Called manually by admin when all majors are complete - * Phase 3.2 Implementation - * - * Q4: Manual finalization (admin button) - * Q5: QP ties handled by sharing placements - * Q19: Tie entry uses same placement, system auto-calculates shared positions + * Finalize qualifying points and convert to fantasy placements. + * Called manually by admin when all events are complete. + * Tied QP totals result in shared placements (averaged points). */ export async function finalizeQualifyingPoints( sportsSeasonId: string, @@ -478,7 +617,7 @@ export async function finalizeQualifyingPoints( // Trigger recalculation for all affected leagues await recalculateAffectedLeagues(sportsSeasonId, db); - // Auto-trigger probability recalculation (Phase 5.3) + // Auto-trigger probability recalculation after result try { await updateProbabilitiesAfterResult(sportsSeasonId, true); console.log( @@ -497,12 +636,9 @@ export async function finalizeQualifyingPoints( } /** - * Process season standings (F1) and assign final placements - * - * Q7: For F1, we show current F1 points during the season, then assign fantasy points at the end - * Q8: Just final standings, not individual races - * Q14: Store current running total, manual update with API future - * Q13/Q19: Ties handled by admin entering same position, system auto-shares placements + * Process season standings (F1/IndyCar) and assign final placements. + * Reads from participant_season_results (championship points) and converts + * final standings positions to fantasy placements. Ties share placements. */ export async function processSeasonStandings( sportsSeasonId: string, @@ -591,7 +727,7 @@ export async function processSeasonStandings( // Trigger recalculation for all affected leagues await recalculateAffectedLeagues(sportsSeasonId, db); - // Auto-trigger probability recalculation (Phase 5.3) + // Auto-trigger probability recalculation after result try { await updateProbabilitiesAfterResult(sportsSeasonId, true); console.log( @@ -641,6 +777,7 @@ export async function calculateTeamScore( participant: { with: { results: true, + sportsSeason: true, }, }, }, @@ -660,10 +797,15 @@ export async function calculateTeamScore( let participantsCompleted = 0; for (const pick of picks) { - const result = pick.participant.results[0]; // Should only be one result per participant + // One result per participant per sports season (enforced by upsertParticipantResult). + // If duplicates exist due to data corruption, the first row wins — acceptable trade-off. + const result = pick.participant.results[0]; if (result && result.finalPosition) { - const points = calculateFantasyPoints(result.finalPosition, scoringRules); + const isBracket = pick.participant.sportsSeason?.scoringPattern === "playoff_bracket"; + const points = isBracket + ? calculateBracketPoints(result.finalPosition, scoringRules) + : calculateFantasyPoints(result.finalPosition, scoringRules); totalPoints += points; participantsCompleted++; @@ -683,8 +825,8 @@ export async function calculateTeamScore( } /** - * Calculate projected total points for a team - * Phase 5.4: Includes actual points from finished participants + EVs from unfinished + * Calculate projected total points for a team. + * Combines actual points from finished participants with EV projections for the rest. */ export async function calculateTeamProjectedScore( teamId: string, @@ -861,10 +1003,9 @@ function assignRanks( } /** - * Recalculate standings for all teams in a season - * Called after any scoring event completes or participant results change - * - * Phase 4.1: Implements tiebreaker logic and ranking + * Recalculate standings for all teams in a season. + * Called after any scoring event completes or participant results change. + * Implements tiebreaker logic: total points, then placement counts (1st, 2nd, …). */ export async function recalculateStandings( seasonId: string, @@ -923,7 +1064,6 @@ export async function recalculateStandings( eighthPlaceCount: teamScore.placementCounts[8], participantsRemaining: teamScore.participantsTotal - teamScore.participantsCompleted, - // Phase 5.4: Projected points tracking actualPoints: teamScore.actualPoints?.toString() ?? null, projectedPoints: teamScore.projectedPoints?.toString() ?? null, participantsFinished: teamScore.participantsFinished ?? null, diff --git a/app/models/scoring-rules.ts b/app/models/scoring-rules.ts index 26fb39c..dc590ab 100644 --- a/app/models/scoring-rules.ts +++ b/app/models/scoring-rules.ts @@ -104,6 +104,34 @@ export function calculateAveragedPoints( return total / placements.length; } +/** + * Calculate fantasy points for a bracket placement, averaging tied positions. + * + * Standard single-elimination bracket tiers: + * 1st: solo winner + * 2nd: solo finalist + * 3rd-4th: two SF losers share these positions → averaged + * 5th-8th: four QF losers share these positions → averaged + * + * Use this instead of calculateFantasyPoints for playoff_bracket scoring. + * + * TODO: This hardcodes 8-team bracket tiers. Future support for larger brackets + * (e.g. 16-team with 9th-16th scoring) will require parameterised tier config. + */ +export function calculateBracketPoints( + finalPosition: number, + rules: ScoringRules +): number { + if (finalPosition <= 0) return 0; + if (finalPosition === 1) return rules.pointsFor1st; + if (finalPosition === 2) return rules.pointsFor2nd; + if (finalPosition === 3 || finalPosition === 4) + return calculateAveragedPoints([3, 4], rules); + if (finalPosition >= 5 && finalPosition <= 8) + return calculateAveragedPoints([5, 6, 7, 8], rules); + return 0; +} + /** * Get points array as a simple ordered list [1st, 2nd, 3rd, ..., 8th] * Useful for display purposes diff --git a/app/models/user.ts b/app/models/user.ts index 3b3467e..fea35d2 100644 --- a/app/models/user.ts +++ b/app/models/user.ts @@ -124,6 +124,9 @@ export async function isUserAdmin(userId: string): Promise { } export async function isUserAdminByClerkId(clerkId: string): Promise { + if (process.env.NODE_ENV === "development" && process.env.DEV_ADMIN_CLERK_ID === clerkId) { + return true; + } const user = await findUserByClerkId(clerkId); return user?.isAdmin ?? false; } diff --git a/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts b/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts index d94c64b..0f0df0d 100644 --- a/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts +++ b/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts @@ -5,10 +5,10 @@ import { isUserLeagueMember, isCommissioner, findTeamsBySeasonId, - findUserByClerkId, } from "~/models"; import { getDraftPicks } from "~/models/draft-pick"; import { getSeasonResults } from "~/models/participant-season-result"; +import { calculateBracketPoints } from "~/models/scoring-rules"; import { getQPStandings } from "~/models/qualifying-points"; import { getUpcomingScoringEvents, @@ -16,7 +16,7 @@ import { } from "~/models/scoring-event"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; -import { eq, and } from "drizzle-orm"; +import { eq, and, inArray } from "drizzle-orm"; export async function loader(args: Route.LoaderArgs) { const { userId } = await getAuth(args); @@ -88,18 +88,17 @@ export async function loader(args: Route.LoaderArgs) { { teamName: string; teamId: string; ownerName?: string } >(); - // Get unique owner IDs for user lookups - const ownerIds = [...new Set(teams.map((t) => t.ownerId).filter(Boolean))]; - const ownerUsers = await Promise.all( - ownerIds.map(async (ownerId) => { - const user = await findUserByClerkId(ownerId!); - return { - ownerId, - name: user?.username || user?.displayName || "Unknown", - }; - }) + // Get unique owner IDs and batch-fetch their user records in a single query + const ownerIds = [...new Set(teams.map((t) => t.ownerId).filter(Boolean))] as string[]; + const ownerUserRows = ownerIds.length > 0 + ? await db.query.users.findMany({ + where: inArray(schema.users.clerkId, ownerIds), + columns: { clerkId: true, username: true, displayName: true }, + }) + : []; + const ownerMap = new Map( + ownerUserRows.map((u) => [u.clerkId, u.username || u.displayName || "Unknown"]) ); - const ownerMap = new Map(ownerUsers.map((o) => [o.ownerId, o.name])); // Map draft picks to ownership for (const pick of draftPicks) { @@ -137,6 +136,7 @@ export async function loader(args: Route.LoaderArgs) { let playoffRounds: string[] = []; let preEliminatedParticipants: { id: string; name: string }[] = []; let participantPoints: { participantId: string; points: number }[] = []; + let partialScoreParticipantIds: string[] = []; let seasonStandings: SeasonStanding[] = []; let qpStandings: any[] = []; @@ -169,8 +169,8 @@ export async function loader(args: Route.LoaderArgs) { ); } - // Fetch group-stage losers and fantasy points in parallel - const [eliminatedResults, seasonResults] = await Promise.all([ + // Fetch group-stage losers and all participant results for bracket scoring + const [eliminatedResults, allResults] = await Promise.all([ db.query.participantResults.findMany({ where: and( eq(schema.participantResults.sportsSeasonId, sportsSeasonId), @@ -178,16 +178,44 @@ export async function loader(args: Route.LoaderArgs) { ), with: { participant: true }, }), - getSeasonResults(sportsSeasonId), + db.query.participantResults.findMany({ + where: eq(schema.participantResults.sportsSeasonId, sportsSeasonId), + }), ]); preEliminatedParticipants = eliminatedResults .filter((r) => r.participant) .map((r) => ({ id: (r as any).participant.id, name: (r as any).participant.name })); - participantPoints = seasonResults - .map((r) => ({ participantId: r.participantId, points: parseFloat(r.currentPoints || "0") })) - .filter((r) => r.points > 0); + // Compute per-participant fantasy points directly from participant_results. + // This covers both finalized placements and progressive floor scores for still-alive participants. + const scoringRules = { + pointsFor1st: season.pointsFor1st, + pointsFor2nd: season.pointsFor2nd, + pointsFor3rd: season.pointsFor3rd, + pointsFor4th: season.pointsFor4th, + pointsFor5th: season.pointsFor5th, + pointsFor6th: season.pointsFor6th, + pointsFor7th: season.pointsFor7th, + pointsFor8th: season.pointsFor8th, + }; + const seenParticipantIds = new Set(); + participantPoints = allResults + .filter((r) => r.finalPosition !== null && r.finalPosition > 0) + .filter((r) => { + if (seenParticipantIds.has(r.participantId)) return false; + seenParticipantIds.add(r.participantId); + return true; + }) + .map((r) => ({ + participantId: r.participantId, + points: calculateBracketPoints(r.finalPosition!, scoringRules), + })); + + // Track which participants have provisional (floor) scores — still competing + partialScoreParticipantIds = allResults + .filter((r) => r.isPartialScore) + .map((r) => r.participantId); } else if (scoringPattern === "season_standings") { // Fetch F1-style championship standings and map to SeasonStanding shape const results = await getSeasonResults(sportsSeasonId); @@ -226,6 +254,7 @@ export async function loader(args: Route.LoaderArgs) { playoffRounds, preEliminatedParticipants, participantPoints, + partialScoreParticipantIds, seasonStandings, qpStandings, teamOwnerships, diff --git a/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.tsx b/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.tsx index 5a5e34f..79e86ea 100644 --- a/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.tsx +++ b/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.tsx @@ -49,6 +49,7 @@ export default function SportSeasonDetail({ playoffRounds, preEliminatedParticipants, participantPoints, + partialScoreParticipantIds, seasonStandings, qpStandings, teamOwnerships, @@ -100,6 +101,7 @@ export default function SportSeasonDetail({ playoffRounds={playoffRounds} preEliminatedParticipants={preEliminatedParticipants} participantPoints={participantPoints} + partialScoreParticipantIds={partialScoreParticipantIds} seasonStandings={seasonStandings as any} seasonIsFinalized={seasonIsFinalized} qpStandings={qpStandings as any} diff --git a/app/services/__tests__/probability-updater.test.ts b/app/services/__tests__/probability-updater.test.ts index 4841b86..c1388ed 100644 --- a/app/services/__tests__/probability-updater.test.ts +++ b/app/services/__tests__/probability-updater.test.ts @@ -32,6 +32,7 @@ describe("probability-updater", () => { participantId: "participant-1", sportsSeasonId: "season-1", finalPosition: 1, + isPartialScore: false, qualifyingPoints: null, notes: null, createdAt: new Date(), @@ -102,6 +103,7 @@ describe("probability-updater", () => { participantId: "participant-1", sportsSeasonId: "season-1", finalPosition: 1, + isPartialScore: false, qualifyingPoints: null, notes: null, createdAt: new Date(), @@ -112,6 +114,7 @@ describe("probability-updater", () => { participantId: "participant-2", sportsSeasonId: "season-1", finalPosition: 2, + isPartialScore: false, qualifyingPoints: null, notes: null, createdAt: new Date(), @@ -187,6 +190,7 @@ describe("probability-updater", () => { participantId: "participant-1", sportsSeasonId: "season-1", finalPosition: null, // No position set yet + isPartialScore: false, qualifyingPoints: "50.00", notes: null, createdAt: new Date(), @@ -210,6 +214,7 @@ describe("probability-updater", () => { participantId: "participant-1", sportsSeasonId: "season-1", finalPosition: 0, // Eliminated + isPartialScore: false, qualifyingPoints: null, notes: null, createdAt: new Date(), diff --git a/database/schema.ts b/database/schema.ts index d93748b..74f230b 100644 --- a/database/schema.ts +++ b/database/schema.ts @@ -318,6 +318,7 @@ export const participantResults = pgTable("participant_results", { .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(), diff --git a/drizzle/0038_sad_wilson_fisk.sql b/drizzle/0038_sad_wilson_fisk.sql new file mode 100644 index 0000000..15a9605 --- /dev/null +++ b/drizzle/0038_sad_wilson_fisk.sql @@ -0,0 +1,2 @@ +-- Progressive floor scoring: track whether a participant result is provisional (still alive) +ALTER TABLE "participant_results" ADD COLUMN "is_partial_score" boolean DEFAULT false NOT NULL; diff --git a/drizzle/meta/0038_snapshot.json b/drizzle/meta/0038_snapshot.json new file mode 100644 index 0000000..8fd7e1d --- /dev/null +++ b/drizzle/meta/0038_snapshot.json @@ -0,0 +1,3303 @@ +{ + "id": "c33ec2dc-da57-4f9d-b094-84b423f95f2a", + "prevId": "5a5b7ce7-0459-461f-8ff0-09f6ce367bd8", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.autodraft_settings": { + "name": "autodraft_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "is_enabled": { + "name": "is_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "mode": { + "name": "mode", + "type": "autodraft_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'next_pick'" + }, + "queue_only": { + "name": "queue_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "autodraft_settings_season_id_seasons_id_fk": { + "name": "autodraft_settings_season_id_seasons_id_fk", + "tableFrom": "autodraft_settings", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "autodraft_settings_team_id_teams_id_fk": { + "name": "autodraft_settings_team_id_teams_id_fk", + "tableFrom": "autodraft_settings", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.commissioners": { + "name": "commissioners", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "league_id": { + "name": "league_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "commissioners_league_id_leagues_id_fk": { + "name": "commissioners_league_id_leagues_id_fk", + "tableFrom": "commissioners", + "tableTo": "leagues", + "columnsFrom": [ + "league_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.draft_picks": { + "name": "draft_picks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pick_number": { + "name": "pick_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "round": { + "name": "round", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pick_in_round": { + "name": "pick_in_round", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "picked_by_user_id": { + "name": "picked_by_user_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "picked_by_type": { + "name": "picked_by_type", + "type": "picked_by_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "time_used": { + "name": "time_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "draft_picks_season_pick_unique": { + "name": "draft_picks_season_pick_unique", + "columns": [ + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pick_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "draft_picks_season_id_seasons_id_fk": { + "name": "draft_picks_season_id_seasons_id_fk", + "tableFrom": "draft_picks", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_picks_team_id_teams_id_fk": { + "name": "draft_picks_team_id_teams_id_fk", + "tableFrom": "draft_picks", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_picks_participant_id_participants_id_fk": { + "name": "draft_picks_participant_id_participants_id_fk", + "tableFrom": "draft_picks", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.draft_queue": { + "name": "draft_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "queue_position": { + "name": "queue_position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "draft_queue_season_id_seasons_id_fk": { + "name": "draft_queue_season_id_seasons_id_fk", + "tableFrom": "draft_queue", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_queue_team_id_teams_id_fk": { + "name": "draft_queue_team_id_teams_id_fk", + "tableFrom": "draft_queue", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_queue_participant_id_participants_id_fk": { + "name": "draft_queue_participant_id_participants_id_fk", + "tableFrom": "draft_queue", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.draft_slots": { + "name": "draft_slots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "draft_order": { + "name": "draft_order", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "draft_slots_season_id_seasons_id_fk": { + "name": "draft_slots_season_id_seasons_id_fk", + "tableFrom": "draft_slots", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_slots_team_id_teams_id_fk": { + "name": "draft_slots_team_id_teams_id_fk", + "tableFrom": "draft_slots", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.draft_timers": { + "name": "draft_timers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "time_remaining": { + "name": "time_remaining", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "draft_timers_season_id_seasons_id_fk": { + "name": "draft_timers_season_id_seasons_id_fk", + "tableFrom": "draft_timers", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "draft_timers_team_id_teams_id_fk": { + "name": "draft_timers_team_id_teams_id_fk", + "tableFrom": "draft_timers", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.event_results": { + "name": "event_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scoring_event_id": { + "name": "scoring_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "placement": { + "name": "placement", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "qualifying_points_awarded": { + "name": "qualifying_points_awarded", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "eliminated": { + "name": "eliminated", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "raw_score": { + "name": "raw_score", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "event_results_scoring_event_id_scoring_events_id_fk": { + "name": "event_results_scoring_event_id_scoring_events_id_fk", + "tableFrom": "event_results", + "tableTo": "scoring_events", + "columnsFrom": [ + "scoring_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "event_results_participant_id_participants_id_fk": { + "name": "event_results_participant_id_participants_id_fk", + "tableFrom": "event_results", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.leagues": { + "name": "leagues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "current_season_id": { + "name": "current_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_public_draft_board": { + "name": "is_public_draft_board", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_ev_snapshots": { + "name": "participant_ev_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "snapshot_date": { + "name": "snapshot_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "prob_first": { + "name": "prob_first", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_second": { + "name": "prob_second", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_third": { + "name": "prob_third", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_fourth": { + "name": "prob_fourth", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_fifth": { + "name": "prob_fifth", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_sixth": { + "name": "prob_sixth", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_seventh": { + "name": "prob_seventh", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_eighth": { + "name": "prob_eighth", + "type": "numeric(5, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "calculated_ev": { + "name": "calculated_ev", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "source": { + "name": "source", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "participant_ev_snapshots_unique": { + "name": "participant_ev_snapshots_unique", + "columns": [ + { + "expression": "participant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sports_season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "snapshot_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "participant_ev_snapshots_participant_id_participants_id_fk": { + "name": "participant_ev_snapshots_participant_id_participants_id_fk", + "tableFrom": "participant_ev_snapshots", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "participant_ev_snapshots_sports_season_id_sports_seasons_id_fk": { + "name": "participant_ev_snapshots_sports_season_id_sports_seasons_id_fk", + "tableFrom": "participant_ev_snapshots", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_expected_values": { + "name": "participant_expected_values", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "prob_first": { + "name": "prob_first", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_second": { + "name": "prob_second", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_third": { + "name": "prob_third", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_fourth": { + "name": "prob_fourth", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_fifth": { + "name": "prob_fifth", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_sixth": { + "name": "prob_sixth", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_seventh": { + "name": "prob_seventh", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "prob_eighth": { + "name": "prob_eighth", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "expected_value": { + "name": "expected_value", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "source": { + "name": "source", + "type": "probability_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": false, + "default": "'manual'" + }, + "source_odds": { + "name": "source_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "calculated_at": { + "name": "calculated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "participant_expected_values_participant_id_participants_id_fk": { + "name": "participant_expected_values_participant_id_participants_id_fk", + "tableFrom": "participant_expected_values", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "participant_expected_values_sports_season_id_sports_seasons_id_fk": { + "name": "participant_expected_values_sports_season_id_sports_seasons_id_fk", + "tableFrom": "participant_expected_values", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_qualifying_totals": { + "name": "participant_qualifying_totals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "total_qualifying_points": { + "name": "total_qualifying_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "events_scored": { + "name": "events_scored", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "final_ranking": { + "name": "final_ranking", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "participant_qualifying_totals_participant_id_participants_id_fk": { + "name": "participant_qualifying_totals_participant_id_participants_id_fk", + "tableFrom": "participant_qualifying_totals", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "participant_qualifying_totals_sports_season_id_sports_seasons_id_fk": { + "name": "participant_qualifying_totals_sports_season_id_sports_seasons_id_fk", + "tableFrom": "participant_qualifying_totals", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_results": { + "name": "participant_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "final_position": { + "name": "final_position", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "is_partial_score": { + "name": "is_partial_score", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "qualifying_points": { + "name": "qualifying_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "participant_results_participant_id_participants_id_fk": { + "name": "participant_results_participant_id_participants_id_fk", + "tableFrom": "participant_results", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "participant_results_sports_season_id_sports_seasons_id_fk": { + "name": "participant_results_sports_season_id_sports_seasons_id_fk", + "tableFrom": "participant_results", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participant_season_results": { + "name": "participant_season_results", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "current_points": { + "name": "current_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_position": { + "name": "current_position", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "participant_season_results_participant_id_participants_id_fk": { + "name": "participant_season_results_participant_id_participants_id_fk", + "tableFrom": "participant_season_results", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "participant_season_results_sports_season_id_sports_seasons_id_fk": { + "name": "participant_season_results_sports_season_id_sports_seasons_id_fk", + "tableFrom": "participant_season_results", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.participants": { + "name": "participants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "short_name": { + "name": "short_name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "expected_value": { + "name": "expected_value", + "type": "numeric(10, 4)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "participants_sports_season_id_sports_seasons_id_fk": { + "name": "participants_sports_season_id_sports_seasons_id_fk", + "tableFrom": "participants", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.playoff_matches": { + "name": "playoff_matches", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scoring_event_id": { + "name": "scoring_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "round": { + "name": "round", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "match_number": { + "name": "match_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "participant1_id": { + "name": "participant1_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "participant2_id": { + "name": "participant2_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "winner_id": { + "name": "winner_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "loser_id": { + "name": "loser_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_complete": { + "name": "is_complete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "participant1_score": { + "name": "participant1_score", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "participant2_score": { + "name": "participant2_score", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "is_scoring": { + "name": "is_scoring", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "template_round": { + "name": "template_round", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "seed_info": { + "name": "seed_info", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "playoff_matches_scoring_event_id_scoring_events_id_fk": { + "name": "playoff_matches_scoring_event_id_scoring_events_id_fk", + "tableFrom": "playoff_matches", + "tableTo": "scoring_events", + "columnsFrom": [ + "scoring_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "playoff_matches_participant1_id_participants_id_fk": { + "name": "playoff_matches_participant1_id_participants_id_fk", + "tableFrom": "playoff_matches", + "tableTo": "participants", + "columnsFrom": [ + "participant1_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "playoff_matches_participant2_id_participants_id_fk": { + "name": "playoff_matches_participant2_id_participants_id_fk", + "tableFrom": "playoff_matches", + "tableTo": "participants", + "columnsFrom": [ + "participant2_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "playoff_matches_winner_id_participants_id_fk": { + "name": "playoff_matches_winner_id_participants_id_fk", + "tableFrom": "playoff_matches", + "tableTo": "participants", + "columnsFrom": [ + "winner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "playoff_matches_loser_id_participants_id_fk": { + "name": "playoff_matches_loser_id_participants_id_fk", + "tableFrom": "playoff_matches", + "tableTo": "participants", + "columnsFrom": [ + "loser_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.qualifying_point_config": { + "name": "qualifying_point_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "placement": { + "name": "placement", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "points": { + "name": "points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "qualifying_point_config_sports_season_id_sports_seasons_id_fk": { + "name": "qualifying_point_config_sports_season_id_sports_seasons_id_fk", + "tableFrom": "qualifying_point_config", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scoring_events": { + "name": "scoring_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "event_date": { + "name": "event_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "event_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "playoff_round": { + "name": "playoff_round", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "is_qualifying_event": { + "name": "is_qualifying_event", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "bracket_template_id": { + "name": "bracket_template_id", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "scoring_starts_at_round": { + "name": "scoring_starts_at_round", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false + }, + "is_complete": { + "name": "is_complete", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "scoring_events_sports_season_id_sports_seasons_id_fk": { + "name": "scoring_events_sports_season_id_sports_seasons_id_fk", + "tableFrom": "scoring_events", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_sports": { + "name": "season_sports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "season_sports_season_id_seasons_id_fk": { + "name": "season_sports_season_id_seasons_id_fk", + "tableFrom": "season_sports", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "season_sports_sports_season_id_sports_seasons_id_fk": { + "name": "season_sports_sports_season_id_sports_seasons_id_fk", + "tableFrom": "season_sports", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_template_sports": { + "name": "season_template_sports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "season_template_sports_template_id_season_templates_id_fk": { + "name": "season_template_sports_template_id_season_templates_id_fk", + "tableFrom": "season_template_sports", + "tableTo": "season_templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "season_template_sports_sports_season_id_sports_seasons_id_fk": { + "name": "season_template_sports_sports_season_id_sports_seasons_id_fk", + "tableFrom": "season_template_sports", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.season_templates": { + "name": "season_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.seasons": { + "name": "seasons", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "league_id": { + "name": "league_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "season_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pre_draft'" + }, + "template_id": { + "name": "template_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "draft_rounds": { + "name": "draft_rounds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 20 + }, + "flex_spots": { + "name": "flex_spots", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "draft_date_time": { + "name": "draft_date_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "draft_initial_time": { + "name": "draft_initial_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 120 + }, + "draft_increment_time": { + "name": "draft_increment_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 30 + }, + "current_pick_number": { + "name": "current_pick_number", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1 + }, + "draft_started_at": { + "name": "draft_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "draft_paused": { + "name": "draft_paused", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "invite_code": { + "name": "invite_code", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true + }, + "points_for_1st": { + "name": "points_for_1st", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "points_for_2nd": { + "name": "points_for_2nd", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 70 + }, + "points_for_3rd": { + "name": "points_for_3rd", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 50 + }, + "points_for_4th": { + "name": "points_for_4th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 40 + }, + "points_for_5th": { + "name": "points_for_5th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 25 + }, + "points_for_6th": { + "name": "points_for_6th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 25 + }, + "points_for_7th": { + "name": "points_for_7th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "points_for_8th": { + "name": "points_for_8th", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "seasons_league_id_leagues_id_fk": { + "name": "seasons_league_id_leagues_id_fk", + "tableFrom": "seasons", + "tableTo": "leagues", + "columnsFrom": [ + "league_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "seasons_template_id_season_templates_id_fk": { + "name": "seasons_template_id_season_templates_id_fk", + "tableFrom": "seasons", + "tableTo": "season_templates", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "seasons_invite_code_unique": { + "name": "seasons_invite_code_unique", + "nullsNotDistinct": false, + "columns": [ + "invite_code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sports": { + "name": "sports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "sport_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon_url": { + "name": "icon_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "simulator_type": { + "name": "simulator_type", + "type": "simulator_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sports_slug_unique": { + "name": "sports_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sports_seasons": { + "name": "sports_seasons", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "sport_id": { + "name": "sport_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "year": { + "name": "year", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "start_date": { + "name": "start_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "end_date": { + "name": "end_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "sports_season_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'upcoming'" + }, + "scoring_type": { + "name": "scoring_type", + "type": "scoring_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "scoring_pattern": { + "name": "scoring_pattern", + "type": "scoring_pattern", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "total_majors": { + "name": "total_majors", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "majors_completed": { + "name": "majors_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "qualifying_points_finalized": { + "name": "qualifying_points_finalized", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "elo_calibration_exponent": { + "name": "elo_calibration_exponent", + "type": "numeric(3, 2)", + "primaryKey": false, + "notNull": false + }, + "elo_min_rating": { + "name": "elo_min_rating", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1250 + }, + "elo_max_rating": { + "name": "elo_max_rating", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 1750 + }, + "simulation_status": { + "name": "simulation_status", + "type": "simulation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sports_seasons_sport_id_sports_id_fk": { + "name": "sports_seasons_sport_id_sports_id_fk", + "tableFrom": "sports_seasons", + "tableTo": "sports", + "columnsFrom": [ + "sport_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_ev_snapshots": { + "name": "team_ev_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "snapshot_date": { + "name": "snapshot_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "projected_points": { + "name": "projected_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "actual_points": { + "name": "actual_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "team_ev_snapshots_unique": { + "name": "team_ev_snapshots_unique", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "season_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "snapshot_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_ev_snapshots_team_id_teams_id_fk": { + "name": "team_ev_snapshots_team_id_teams_id_fk", + "tableFrom": "team_ev_snapshots", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_ev_snapshots_season_id_seasons_id_fk": { + "name": "team_ev_snapshots_season_id_seasons_id_fk", + "tableFrom": "team_ev_snapshots", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_sport_scores": { + "name": "team_sport_scores", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sports_season_id": { + "name": "sports_season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "total_points": { + "name": "total_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "participants_completed": { + "name": "participants_completed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "participants_total": { + "name": "participants_total", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "calculated_at": { + "name": "calculated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_sport_scores_team_id_teams_id_fk": { + "name": "team_sport_scores_team_id_teams_id_fk", + "tableFrom": "team_sport_scores", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_sport_scores_sports_season_id_sports_seasons_id_fk": { + "name": "team_sport_scores_sports_season_id_sports_seasons_id_fk", + "tableFrom": "team_sport_scores", + "tableTo": "sports_seasons", + "columnsFrom": [ + "sports_season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_standings": { + "name": "team_standings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "total_points": { + "name": "total_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_rank": { + "name": "current_rank", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "previous_rank": { + "name": "previous_rank", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "first_place_count": { + "name": "first_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "second_place_count": { + "name": "second_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "third_place_count": { + "name": "third_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fourth_place_count": { + "name": "fourth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fifth_place_count": { + "name": "fifth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sixth_place_count": { + "name": "sixth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "seventh_place_count": { + "name": "seventh_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "eighth_place_count": { + "name": "eighth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "participants_remaining": { + "name": "participants_remaining", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "actual_points": { + "name": "actual_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "projected_points": { + "name": "projected_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "participants_finished": { + "name": "participants_finished", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "calculated_at": { + "name": "calculated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_standings_team_id_teams_id_fk": { + "name": "team_standings_team_id_teams_id_fk", + "tableFrom": "team_standings", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_standings_season_id_seasons_id_fk": { + "name": "team_standings_season_id_seasons_id_fk", + "tableFrom": "team_standings", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.team_standings_snapshots": { + "name": "team_standings_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "snapshot_date": { + "name": "snapshot_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "total_points": { + "name": "total_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "rank": { + "name": "rank", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "first_place_count": { + "name": "first_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "second_place_count": { + "name": "second_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "third_place_count": { + "name": "third_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fourth_place_count": { + "name": "fourth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "fifth_place_count": { + "name": "fifth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sixth_place_count": { + "name": "sixth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "seventh_place_count": { + "name": "seventh_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "eighth_place_count": { + "name": "eighth_place_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "participants_remaining": { + "name": "participants_remaining", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "actual_points": { + "name": "actual_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "projected_points": { + "name": "projected_points", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "participants_finished": { + "name": "participants_finished", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "team_standings_snapshots_team_id_teams_id_fk": { + "name": "team_standings_snapshots_team_id_teams_id_fk", + "tableFrom": "team_standings_snapshots", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_standings_snapshots_season_id_seasons_id_fk": { + "name": "team_standings_snapshots_season_id_seasons_id_fk", + "tableFrom": "team_standings_snapshots", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "season_id": { + "name": "season_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "logo_url": { + "name": "logo_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "teams_season_id_seasons_id_fk": { + "name": "teams_season_id_seasons_id_fk", + "tableFrom": "teams", + "tableTo": "seasons", + "columnsFrom": [ + "season_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tournament_group_members": { + "name": "tournament_group_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tournament_group_id": { + "name": "tournament_group_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "eliminated": { + "name": "eliminated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tournament_group_members_tournament_group_id_tournament_groups_id_fk": { + "name": "tournament_group_members_tournament_group_id_tournament_groups_id_fk", + "tableFrom": "tournament_group_members", + "tableTo": "tournament_groups", + "columnsFrom": [ + "tournament_group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tournament_group_members_participant_id_participants_id_fk": { + "name": "tournament_group_members_participant_id_participants_id_fk", + "tableFrom": "tournament_group_members", + "tableTo": "participants", + "columnsFrom": [ + "participant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tournament_groups": { + "name": "tournament_groups", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "scoring_event_id": { + "name": "scoring_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "group_name": { + "name": "group_name", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "tournament_groups_scoring_event_id_scoring_events_id_fk": { + "name": "tournament_groups_scoring_event_id_scoring_events_id_fk", + "tableFrom": "tournament_groups", + "tableTo": "scoring_events", + "columnsFrom": [ + "scoring_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "clerk_id": { + "name": "clerk_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "image_url": { + "name": "image_url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": false + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_clerk_id_unique": { + "name": "users_clerk_id_unique", + "nullsNotDistinct": false, + "columns": [ + "clerk_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.autodraft_mode": { + "name": "autodraft_mode", + "schema": "public", + "values": [ + "next_pick", + "while_on" + ] + }, + "public.event_type": { + "name": "event_type", + "schema": "public", + "values": [ + "playoff_game", + "major_tournament", + "final_standings", + "schedule_event" + ] + }, + "public.picked_by_type": { + "name": "picked_by_type", + "schema": "public", + "values": [ + "owner", + "commissioner", + "auto" + ] + }, + "public.probability_source": { + "name": "probability_source", + "schema": "public", + "values": [ + "manual", + "futures_odds", + "elo_simulation", + "performance_model" + ] + }, + "public.scoring_pattern": { + "name": "scoring_pattern", + "schema": "public", + "values": [ + "playoff_bracket", + "season_standings", + "qualifying_points" + ] + }, + "public.scoring_type": { + "name": "scoring_type", + "schema": "public", + "values": [ + "playoffs", + "regular_season", + "majors" + ] + }, + "public.season_status": { + "name": "season_status", + "schema": "public", + "values": [ + "pre_draft", + "draft", + "active", + "completed" + ] + }, + "public.simulation_status": { + "name": "simulation_status", + "schema": "public", + "values": [ + "idle", + "running", + "failed" + ] + }, + "public.simulator_type": { + "name": "simulator_type", + "schema": "public", + "values": [ + "f1_standings", + "indycar_standings", + "golf_qualifying_points", + "playoff_bracket" + ] + }, + "public.sport_type": { + "name": "sport_type", + "schema": "public", + "values": [ + "team", + "individual" + ] + }, + "public.sports_season_status": { + "name": "sports_season_status", + "schema": "public", + "values": [ + "upcoming", + "active", + "completed" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index b799f5c..e027ad9 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -267,6 +267,13 @@ "when": 1763289000000, "tag": "0037_add_simulator_type_to_sports", "breakpoints": true + }, + { + "idx": 38, + "version": "7", + "when": 1773122866361, + "tag": "0038_sad_wilson_fisk", + "breakpoints": true } ] } \ No newline at end of file