From dbffddc9ff689cea74e15f3ee422b6207eada712 Mon Sep 17 00:00:00 2001 From: Chris Parsons <438676+chrisparsons83@users.noreply.github.com> Date: Tue, 17 Mar 2026 10:50:30 -0700 Subject: [PATCH] Partial bracket scoring, code review fixes, and double-chance logic (#156) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Partial bracket scoring - `processMatchResult`: new exported function that scores a single match immediately (loser → final placement, winner → provisional floor). Called from `set-winner` and `set-round-winners` so points are awarded as soon as a winner is set, before the full round is complete. - `set-winner`: passes `eventName` to `processMatchResult`. - `set-round-winners`: batches side effects — calls `recalculateAffectedLeagues` and `updateProbabilitiesAfterResult` once after the loop instead of per-match (`skipSideEffects: true` per match). ## Code review fixes - **ROUND_CONFIG** (S1): extracted a `RoundScoringConfig` lookup table + `getRoundConfig()` helper, eliminating three parallel `if/else` chains in `processPlayoffEvent`, `processMatchResult`, and `getGuaranteedMinimumPosition`. AFL template overrides live in `TEMPLATE_ROUND_CONFIG` with an explanatory comment about why the `bracketTemplateId` guard is required. - **skipSideEffects** (C2): new param on `processMatchResult`; bracket server uses it to batch standings/probability recalc in `set-round-winners`. - **eventName** (W1): passed through `processMatchResult` → `recalculateAffectedLeagues`. - **Round validation** (W2): `set-round-winners` now guards `match.round === round` before processing each assignment. - **Comments** (C3/S3/W3): added notes on non-scoring loser assumption, `isScoring ?? true` default, and AFL Semi-Finals template requirement. ## PlayoffBracket eliminated-teams fix + tests - Fixed `computeEliminatedByRound` to track participant *appearances* (not just wins), so AFL QF losers who advance to Semi-Finals via double-chance are correctly excluded from the QF eliminated list. - Extracted the logic as an exported pure function for testability. - Added 4 tests: standard elimination, AFL QF loser → SF win, AFL QF loser → SF loss, and normal advancement not protecting a later loser. Co-authored-by: Claude Sonnet 4.6 --- app/components/scoring/PlayoffBracket.tsx | 60 +- .../scoring/__tests__/PlayoffBracket.test.tsx | 100 +- .../__tests__/process-match-result.test.ts | 242 ++ app/models/league.ts | 1 + app/models/scoring-calculator.ts | 419 +- ...sons.$id.events.$eventId.bracket.server.ts | 51 +- .../leagues/__tests__/settings-update.test.ts | 1 + app/services/discord.ts | 61 + database/schema.ts | 1 + drizzle/0048_mean_enchantress.sql | 19 + drizzle/meta/0048_snapshot.json | 3543 +++++++++++++++++ drizzle/meta/_journal.json | 9 +- 12 files changed, 4320 insertions(+), 187 deletions(-) create mode 100644 app/models/__tests__/process-match-result.test.ts create mode 100644 app/services/discord.ts create mode 100644 drizzle/0048_mean_enchantress.sql create mode 100644 drizzle/meta/0048_snapshot.json diff --git a/app/components/scoring/PlayoffBracket.tsx b/app/components/scoring/PlayoffBracket.tsx index d5c0994..7dbfad4 100644 --- a/app/components/scoring/PlayoffBracket.tsx +++ b/app/components/scoring/PlayoffBracket.tsx @@ -110,6 +110,54 @@ interface EliminatedEntry { ownership: TeamOwnership | null; } +/** + * For each complete match, determine which losers should be shown as eliminated + * in that round. In double-chance brackets (e.g. AFL), a participant who lost + * round X but then won a later round Y is NOT eliminated at round X — only their + * final loss counts. + * + * Returns a map of round name → array of eliminated participant IDs. + * Exported for unit testing. + */ +export function computeEliminatedByRound( + matches: Array<{ + round: string; + isComplete: boolean; + winnerId: string | null; + loser?: { id: string; name: string } | null; + }>, + rounds: string[] +): Map { + const roundIndex = new Map(rounds.map((r, i) => [r, i])); + + // Track the latest round (by index) each participant has APPEARED IN (as winner OR loser). + // Tracking appearances — not just wins — correctly handles AFL double-chance brackets, + // where a Qualifying Finals loser advances to Semi-Finals automatically (no intermediate + // win required). If a participant appears in a later round, they are not yet eliminated. + const participantLatestRound = new Map(); + const updateLatest = (id: string, ri: number) => { + const prev = participantLatestRound.get(id) ?? -1; + if (ri > prev) participantLatestRound.set(id, ri); + }; + for (const match of matches) { + const ri = roundIndex.get(match.round) ?? -1; + if (match.winnerId) updateLatest(match.winnerId, ri); + if (match.loser?.id) updateLatest(match.loser.id, ri); + } + + const result = new Map(); + for (const match of matches) { + if (!match.isComplete || !match.loser) continue; + const lossRoundIndex = roundIndex.get(match.round) ?? -1; + const loserLatestAppear = participantLatestRound.get(match.loser.id) ?? -1; + // Double-chance survivor: participant appeared in a LATER round — not yet eliminated here. + if (loserLatestAppear > lossRoundIndex) continue; + if (!result.has(match.round)) result.set(match.round, []); + result.get(match.round)!.push(match.loser.id); + } + return result; +} + export function PlayoffBracket({ matches, rounds, @@ -150,17 +198,13 @@ 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); - } + // Compute which participants are eliminated in which round, handling double-chance. + const eliminatedByRound = computeEliminatedByRound(matches, rounds); 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 eliminatedInRound = eliminatedByRound.get(match.round); + if (!eliminatedInRound?.includes(match.loser.id)) continue; const loserScore = match.loserId === match.participant1Id ? match.participant1Score diff --git a/app/components/scoring/__tests__/PlayoffBracket.test.tsx b/app/components/scoring/__tests__/PlayoffBracket.test.tsx index 4536fdb..acb1ca6 100644 --- a/app/components/scoring/__tests__/PlayoffBracket.test.tsx +++ b/app/components/scoring/__tests__/PlayoffBracket.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { buildFeederMap, groupMatchesByRound } from "../PlayoffBracket"; +import { buildFeederMap, groupMatchesByRound, computeEliminatedByRound } from "../PlayoffBracket"; // --------------------------------------------------------------------------- // Helpers @@ -136,3 +136,101 @@ describe("buildFeederMap", () => { expect(map.get("Quarterfinals:4:p2")).toEqual({ round: "Round of 16", matchNumber: 8 }); }); }); + +// --------------------------------------------------------------------------- +// computeEliminatedByRound — double-chance bracket logic +// --------------------------------------------------------------------------- + +type TestMatch = Parameters[0][number]; + +function makeCompleteMatch( + round: string, + winnerId: string, + loserId: string +): TestMatch { + return { + round, + isComplete: true, + winnerId, + loser: { id: loserId, name: loserId }, + }; +} + +describe("computeEliminatedByRound", () => { + describe("standard single-elimination (no double-chance)", () => { + it("a R16 loser who never wins again IS shown as eliminated at R16", () => { + const rounds = ["Round of 16", "Quarterfinals", "Semifinals", "Finals"]; + const matches: TestMatch[] = [ + // teamA wins R16; teamB loses R16 and has no further matches + makeCompleteMatch("Round of 16", "teamA", "teamB"), + makeCompleteMatch("Quarterfinals", "teamC", "teamA"), // teamA loses QF + ]; + + const result = computeEliminatedByRound(matches, rounds); + + // teamB is eliminated at R16 (never won a later round) + expect(result.get("Round of 16")).toContain("teamB"); + // teamA is eliminated at QF (won R16, but lost QF) + expect(result.get("Quarterfinals")).toContain("teamA"); + expect(result.get("Round of 16")).not.toContain("teamA"); + }); + }); + + describe("AFL double-chance bracket", () => { + // AFL afl_10 round order: Qualifying Finals → Elimination Finals → + // Semi-Finals → Preliminary Finals → Grand Final + const AFL_ROUNDS = [ + "Qualifying Finals", + "Elimination Finals", + "Semi-Finals", + "Preliminary Finals", + "Grand Final", + ]; + + it("QF loser who wins a later Semi-Final is NOT shown as eliminated at QF", () => { + const matches: TestMatch[] = [ + // teamA loses Qualifying Finals (double-chance — they come back via Semi-Finals) + makeCompleteMatch("Qualifying Finals", "teamB", "teamA"), + // teamA wins Semi-Finals comeback match + makeCompleteMatch("Semi-Finals", "teamA", "teamC"), + ]; + + const result = computeEliminatedByRound(matches, AFL_ROUNDS); + + // teamA should NOT appear as eliminated at Qualifying Finals + expect(result.get("Qualifying Finals") ?? []).not.toContain("teamA"); + // teamC (who lost the Semi-Final to teamA) IS eliminated + expect(result.get("Semi-Finals")).toContain("teamC"); + }); + + it("QF loser who LATER loses their Semi-Final comeback IS shown as eliminated at Semi-Finals", () => { + const matches: TestMatch[] = [ + makeCompleteMatch("Qualifying Finals", "teamB", "teamA"), + // teamA comes back via Semi-Finals but loses + makeCompleteMatch("Semi-Finals", "teamC", "teamA"), + ]; + + const result = computeEliminatedByRound(matches, AFL_ROUNDS); + + // teamA finally eliminated at Semi-Finals, not at Qualifying Finals + expect(result.get("Qualifying Finals") ?? []).not.toContain("teamA"); + expect(result.get("Semi-Finals")).toContain("teamA"); + }); + + it("winning an EARLIER round (normal advancement) does not protect a later loser", () => { + // Standard case: teamA wins R1, then loses R2 — they ARE eliminated at R2 + const rounds = ["Round of 16", "Quarterfinals", "Semifinals"]; + const matches: TestMatch[] = [ + makeCompleteMatch("Round of 16", "teamA", "teamZ"), + makeCompleteMatch("Quarterfinals", "teamB", "teamA"), + ]; + + const result = computeEliminatedByRound(matches, rounds); + + // teamA's R16 win is normal advancement, not double-chance + expect(result.get("Quarterfinals")).toContain("teamA"); + // teamA should not appear in R16 eliminated list + expect(result.get("Round of 16") ?? []).not.toContain("teamA"); + }); + }); +}); diff --git a/app/models/__tests__/process-match-result.test.ts b/app/models/__tests__/process-match-result.test.ts new file mode 100644 index 0000000..0d19abf --- /dev/null +++ b/app/models/__tests__/process-match-result.test.ts @@ -0,0 +1,242 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; + +// Mock external DB-touching dependencies before importing the module under test +vi.mock("~/services/probability-updater", () => ({ + updateProbabilitiesAfterResult: vi.fn().mockResolvedValue(undefined), +})); + +// ── Minimal db mock ──────────────────────────────────────────────────────── +// +// processMatchResult calls, in order: +// 1. upsertParticipantResult (x2) → db.query.participantResults.findFirst, db.insert +// 2. recalculateAffectedLeagues → db.query.seasonSports.findMany (returns [] so loop exits) +// 3. updateProbabilitiesAfterResult (mocked above) + +function makeDb(existingResult?: { id: string; isPartialScore: boolean }) { + // Track all inserted values in order + const insertedRows: object[] = []; + const updatedRows: object[] = []; + + const insertValues = vi.fn().mockImplementation((values: object) => { + insertedRows.push(values); + return Promise.resolve(); + }); + + const updateWhere = vi.fn().mockResolvedValue(undefined); + const updateSet = vi.fn().mockImplementation((values: object) => { + updatedRows.push(values); + return { where: updateWhere }; + }); + + // participantResults.findFirst: first call gets existingResult (if any), + // subsequent calls return undefined (winner/loser don't have existing rows) + let findFirstCallCount = 0; + const findFirst = vi.fn().mockImplementation(() => { + const result = findFirstCallCount === 0 ? existingResult : undefined; + findFirstCallCount++; + return Promise.resolve(result); + }); + + return { + db: { + insert: vi.fn().mockReturnValue({ values: insertValues }), + update: vi.fn().mockReturnValue({ set: updateSet }), + query: { + participantResults: { findFirst }, + seasonSports: { + findMany: vi.fn().mockResolvedValue([]), // empty → standings loop exits immediately + }, + scoringEvents: { findFirst: vi.fn().mockResolvedValue(null) }, + }, + } as any, + insertedRows, + updatedRows, + findFirst, + insertValues, + updateSet, + updateWhere, + }; +} + +import { processMatchResult } from "../scoring-calculator"; +import { updateProbabilitiesAfterResult } from "~/services/probability-updater"; + +const BASE = { + winnerId: "winner-1", + loserId: "loser-1", + sportsSeasonId: "ss-1", + bracketTemplateId: null as string | null, +}; + +describe("processMatchResult", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("Quarterfinals (scoring)", () => { + it("loser gets position 5 (final), winner gets position 3 (provisional)", async () => { + const { db, insertedRows } = makeDb(); + + await processMatchResult( + { ...BASE, round: "Quarterfinals", isScoring: true }, + db + ); + + expect(insertedRows).toHaveLength(2); + expect(insertedRows[0]).toMatchObject({ participantId: "loser-1", finalPosition: 5, isPartialScore: false }); + expect(insertedRows[1]).toMatchObject({ participantId: "winner-1", finalPosition: 3, isPartialScore: true }); + }); + + it("also works for Elite Eight / Divisional / Conference Semifinals", async () => { + for (const round of ["Elite Eight", "Divisional", "Conference Semifinals"]) { + const { db, insertedRows } = makeDb(); + await processMatchResult({ ...BASE, round, isScoring: true }, db); + expect(insertedRows[0]).toMatchObject({ finalPosition: 5, isPartialScore: false }); + expect(insertedRows[1]).toMatchObject({ finalPosition: 3, isPartialScore: true }); + } + }); + + it("recalculates standings (calls db.query.seasonSports.findMany)", async () => { + const { db } = makeDb(); + await processMatchResult({ ...BASE, round: "Quarterfinals", isScoring: true }, db); + expect((db.query.seasonSports.findMany as ReturnType)).toHaveBeenCalled(); + }); + }); + + describe("Semifinals (scoring)", () => { + it("loser gets position 3 (final), winner gets position 2 (provisional)", async () => { + const { db, insertedRows } = makeDb(); + + await processMatchResult({ ...BASE, round: "Semifinals", isScoring: true }, db); + + expect(insertedRows[0]).toMatchObject({ participantId: "loser-1", finalPosition: 3, isPartialScore: false }); + expect(insertedRows[1]).toMatchObject({ participantId: "winner-1", finalPosition: 2, isPartialScore: true }); + }); + + it("also works for Final Four / Conference Finals / Conference Championship", async () => { + for (const round of ["Final Four", "Conference Finals", "Conference Championship"]) { + const { db, insertedRows } = makeDb(); + await processMatchResult({ ...BASE, round, isScoring: true }, db); + expect(insertedRows[0]).toMatchObject({ finalPosition: 3, isPartialScore: false }); + expect(insertedRows[1]).toMatchObject({ finalPosition: 2, isPartialScore: true }); + } + }); + }); + + describe("Finals / Championship (scoring)", () => { + it("loser=2nd (final), winner=1st (final), no third row", async () => { + const { db, insertedRows } = makeDb(); + + await processMatchResult({ ...BASE, round: "Finals", isScoring: true }, db); + + expect(insertedRows).toHaveLength(2); + expect(insertedRows[0]).toMatchObject({ participantId: "loser-1", finalPosition: 2, isPartialScore: false }); + expect(insertedRows[1]).toMatchObject({ participantId: "winner-1", finalPosition: 1, isPartialScore: false }); + }); + + it("handles all Finals aliases the same way", async () => { + for (const round of ["Championship", "Super Bowl", "NBA Finals", "Grand Final"]) { + const { db, insertedRows } = makeDb(); + await processMatchResult({ ...BASE, round, isScoring: true }, db); + expect(insertedRows).toHaveLength(2); + expect(insertedRows[0]).toMatchObject({ finalPosition: 2, isPartialScore: false }); + expect(insertedRows[1]).toMatchObject({ finalPosition: 1, isPartialScore: false }); + } + }); + }); + + describe("Non-scoring round (isScoring=false)", () => { + it("loser eliminated (pos 0 final), winner gets provisional T5 floor", async () => { + const { db, insertedRows } = makeDb(); + + await processMatchResult({ ...BASE, round: "Round of 16", isScoring: false }, db); + + expect(insertedRows[0]).toMatchObject({ participantId: "loser-1", finalPosition: 0, isPartialScore: false }); + expect(insertedRows[1]).toMatchObject({ participantId: "winner-1", finalPosition: 5, isPartialScore: true }); + }); + }); + + describe("Unrecognized scoring round", () => { + it("gives loser 0 pts and still completes without throwing", async () => { + const { db, insertedRows } = makeDb(); + + await processMatchResult({ ...BASE, round: "Mystery Round", isScoring: true }, db); + + expect(insertedRows[0]).toMatchObject({ finalPosition: 0, isPartialScore: false }); + }); + }); + + describe("AFL rounds", () => { + it("Elimination Finals: loser exits at position 7 (final)", async () => { + const { db, insertedRows } = makeDb(); + + await processMatchResult( + { ...BASE, round: "Elimination Finals", isScoring: true, bracketTemplateId: "afl_10" }, + db + ); + + expect(insertedRows[0]).toMatchObject({ participantId: "loser-1", finalPosition: 7, isPartialScore: false }); + }); + + it("Qualifying Finals: loser is still alive (provisional floor 5)", async () => { + const { db, insertedRows } = makeDb(); + + await processMatchResult( + { ...BASE, round: "Qualifying Finals", isScoring: true, bracketTemplateId: "afl_10" }, + db + ); + + // Loser row is provisional (isPartialScore=true) + expect(insertedRows[0]).toMatchObject({ participantId: "loser-1", finalPosition: 5, isPartialScore: true }); + }); + }); + + describe("Idempotency: already-final row is not un-finalized", () => { + it("does not overwrite a finalized row with a partial score", async () => { + // findFirst returns a finalized row for the first participant (loser) + const existing = { id: "result-1", isPartialScore: false }; + const { db, insertedRows, updatedRows } = makeDb(existing); + + await processMatchResult( + { ...BASE, round: "Quarterfinals", isScoring: true }, + db + ); + + // The guard in upsertParticipantResult should skip the update when + // existing.isPartialScore=false and we're trying to write isPartialScore=true. + // The loser row would try to write isPartialScore=false (final) — that IS + // allowed (both false). The winner row tries isPartialScore=true — since + // there's no existing row for the winner (findFirst returns undefined on + // second call), it inserts. + // + // Key assertion: no update/insert with isPartialScore=true was applied to + // the existing finalized row. + const finalizedOverwrite = updatedRows.find( + (r: any) => r.isPartialScore === true + ); + expect(finalizedOverwrite).toBeUndefined(); + }); + }); + + describe("Probability recalculation", () => { + it("always calls updateProbabilitiesAfterResult with the sports season id", async () => { + const { db } = makeDb(); + + await processMatchResult({ ...BASE, round: "Quarterfinals", isScoring: true }, db); + + expect(updateProbabilitiesAfterResult).toHaveBeenCalledWith("ss-1", true); + }); + + it("does not throw even if probability update fails", async () => { + (updateProbabilitiesAfterResult as ReturnType).mockRejectedValueOnce( + new Error("network error") + ); + const { db } = makeDb(); + + // Should not throw — the error is caught and logged + await expect( + processMatchResult({ ...BASE, round: "Quarterfinals", isScoring: true }, db) + ).resolves.not.toThrow(); + }); + }); +}); diff --git a/app/models/league.ts b/app/models/league.ts index 50537e0..ff1ea44 100644 --- a/app/models/league.ts +++ b/app/models/league.ts @@ -77,6 +77,7 @@ export async function findLeaguesWithActiveSeasonsByUserId( createdBy: schema.leagues.createdBy, currentSeasonId: schema.leagues.currentSeasonId, isPublicDraftBoard: schema.leagues.isPublicDraftBoard, + discordWebhookUrl: schema.leagues.discordWebhookUrl, createdAt: schema.leagues.createdAt, updatedAt: schema.leagues.updatedAt, }; diff --git a/app/models/scoring-calculator.ts b/app/models/scoring-calculator.ts index 2e74fd1..078838a 100644 --- a/app/models/scoring-calculator.ts +++ b/app/models/scoring-calculator.ts @@ -4,6 +4,7 @@ import { eq, and, inArray } from "drizzle-orm"; import { getScoringRules, calculateFantasyPoints, calculateAveragedPoints, calculateBracketPoints } from "./scoring-rules"; import { getSeasonResults } from "./participant-season-result"; import { updateProbabilitiesAfterResult } from "~/services/probability-updater"; +import { sendStandingsUpdateNotification } from "~/services/discord"; /** * Core scoring calculation engine @@ -15,6 +16,79 @@ export type ScoringPattern = | "season_standings" | "qualifying_points"; +// ── Round scoring configuration ────────────────────────────────────────────── + +interface RoundScoringConfig { + /** Final position assigned to the loser of this round. */ + loserPosition: number; + /** true = loser is still competing (AFL double-chance); false = loser is eliminated. */ + loserIsPartial: boolean; + /** + * Guaranteed minimum position for the winner. + * null signals a Finals round: winner is finalized as 1st place (handled inline). + */ + winnerFloor: number | null; +} + +/** + * Standard round name → scoring config mapping. + * Covers all common bracket formats (NCAA, NBA, NFL, Darts, Snooker, AFL). + */ +const ROUND_CONFIG: Record = { + // ── Finals equivalents ───────────────────────────────────────────────────── + Finals: { loserPosition: 2, loserIsPartial: false, winnerFloor: null }, + Championship: { loserPosition: 2, loserIsPartial: false, winnerFloor: null }, + "Super Bowl": { loserPosition: 2, loserIsPartial: false, winnerFloor: null }, + "NBA Finals": { loserPosition: 2, loserIsPartial: false, winnerFloor: null }, + "Grand Final": { loserPosition: 2, loserIsPartial: false, winnerFloor: null }, + // ── Semi-final equivalents ───────────────────────────────────────────────── + Semifinals: { loserPosition: 3, loserIsPartial: false, winnerFloor: 2 }, + "Final Four": { loserPosition: 3, loserIsPartial: false, winnerFloor: 2 }, + "Conference Finals": { loserPosition: 3, loserIsPartial: false, winnerFloor: 2 }, + "Conference Championship": { loserPosition: 3, loserIsPartial: false, winnerFloor: 2 }, + "Preliminary Finals": { loserPosition: 3, loserIsPartial: false, winnerFloor: 2 }, + // ── Quarter-final equivalents ────────────────────────────────────────────── + Quarterfinals: { loserPosition: 5, loserIsPartial: false, winnerFloor: 3 }, + "Elite Eight": { loserPosition: 5, loserIsPartial: false, winnerFloor: 3 }, + Divisional: { loserPosition: 5, loserIsPartial: false, winnerFloor: 3 }, + "Conference Semifinals": { loserPosition: 5, loserIsPartial: false, winnerFloor: 3 }, + // ── AFL-specific rounds ──────────────────────────────────────────────────── + // Qualifying Finals losers get a second chance via Semi-Finals (double-chance). + "Qualifying Finals": { loserPosition: 5, loserIsPartial: true, winnerFloor: 3 }, + // Elimination Finals losers are permanently out (7th–8th). + "Elimination Finals": { loserPosition: 7, loserIsPartial: false, winnerFloor: 5 }, +}; + +/** + * Per-template round config overrides. + * Some round names mean different things in different bracket formats. + * + * IMPORTANT: Template overrides only apply when bracketTemplateId matches exactly. + * For example, "Semi-Finals" in afl_10 eliminates the loser (5th–6th), while a + * generic "Semi-Finals" round (without afl_10 template) is not in ROUND_CONFIG at + * all — use "Semifinals" (no hyphen) for standard brackets. + */ +const TEMPLATE_ROUND_CONFIG: Record> = { + afl_10: { + "Semi-Finals": { loserPosition: 5, loserIsPartial: false, winnerFloor: 3 }, + }, +}; + +/** + * Look up the scoring config for a given round name, applying any template-specific + * overrides before falling back to the standard ROUND_CONFIG. + * Returns null for unrecognized rounds. + */ +function getRoundConfig( + round: string, + bracketTemplateId?: string | null +): RoundScoringConfig | null { + if (bracketTemplateId && TEMPLATE_ROUND_CONFIG[bracketTemplateId]?.[round]) { + return TEMPLATE_ROUND_CONFIG[bracketTemplateId][round]; + } + return ROUND_CONFIG[round] ?? null; +} + /** * Process a playoff event completion and assign final placements. * @@ -126,143 +200,60 @@ export async function processPlayoffEvent( throw new Error(`Scoring rules not found for season ${seasonSport.seasonId}`); } - // 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) { + // Non-scoring (pre-bracket) round: losers are permanently eliminated (0 pts). + // Assumption: once eliminated in a non-scoring round a participant cannot + // re-enter the bracket. Winners bank a provisional T5–T8 floor immediately + // (guaranteed top-8 finish at worst for standard 8-team scoring brackets). for (const match of matches) { if (match.loserId) { - await upsertParticipantResult( - match.loserId, - event.sportsSeasonId, - 0, // 0 = pre-bracket elimination, earns no points - db - ); + await upsertParticipantResult(match.loserId, event.sportsSeasonId, 0, db); } if (match.winnerId) { - await upsertParticipantResult( - match.winnerId, - event.sportsSeasonId, - 5, // provisional T5–T8 floor: guaranteed top-8 finish - db, - true - ); - } - } - } - // 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) { - await upsertParticipantResult( - match.loserId, - event.sportsSeasonId, - 7, // Store as placement 7 (first shared placement for 7-8) - db - ); - } - } - } else if (round === "Semi-Finals" && event.bracketTemplateId === "afl_10") { - // AFL: Semi-Finals losers share 5th-6th - for (const match of matches) { - if (match.loserId) { - await upsertParticipantResult( - match.loserId, - event.sportsSeasonId, - 5, // Store as placement 5 (first shared placement for 5-6) - db - ); - } - } - } - // Standard playoff rounds - else if (round === "Finals" || round === "Championship" || round === "Super Bowl" || round === "NBA Finals" || round === "Grand Final") { - // Winner gets 1st, loser gets 2nd - const finalMatch = matches[0]; - if (!finalMatch || !finalMatch.winnerId || !finalMatch.loserId) { - throw new Error("Finals match is not complete"); - } - - // Update or create participant results for winner (finalized, isPartialScore=false) - await upsertParticipantResult( - finalMatch.winnerId, - event.sportsSeasonId, - 1, - db - ); - - // Update or create participant results for loser (finalized, isPartialScore=false) - await upsertParticipantResult( - finalMatch.loserId, - event.sportsSeasonId, - 2, - db - ); - } else if (round === "Semifinals" || round === "Final Four" || round === "Conference Finals" || round === "Conference Championship" || round === "Preliminary Finals") { - // Losers share 3rd/4th - for (const match of matches) { - if (match.loserId) { - await upsertParticipantResult( - match.loserId, - event.sportsSeasonId, - 3, // Store as placement 3 (the first shared placement) - db - ); - } - } - } else if (round === "Quarterfinals" || round === "Elite Eight" || round === "Divisional" || round === "Conference Semifinals") { - // Losers share 5th-8th - for (const match of matches) { - if (match.loserId) { - await upsertParticipantResult( - match.loserId, - event.sportsSeasonId, - 5, // Store as placement 5 (the first shared placement) - db - ); + await upsertParticipantResult(match.winnerId, event.sportsSeasonId, 5, db, true); } } } 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 - ); + const config = getRoundConfig(round, event.bracketTemplateId); + + if (config === null) { + // Unrecognized scoring round: losers earn 0 pts (outside top-8). + 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); + } + } + } else if (config.winnerFloor === null) { + // Finals: finalize both participants — winner gets 1st, loser gets 2nd. + const finalMatch = matches[0]; + if (!finalMatch || !finalMatch.winnerId || !finalMatch.loserId) { + throw new Error("Finals match is not complete"); + } + await upsertParticipantResult(finalMatch.winnerId, event.sportsSeasonId, 1, db); + await upsertParticipantResult(finalMatch.loserId, event.sportsSeasonId, config.loserPosition, db); + } else { + // All other scoring rounds: assign loser's final (or provisional) position. + for (const match of matches) { + if (match.loserId) { + await upsertParticipantResult( + match.loserId, + event.sportsSeasonId, + config.loserPosition, + db, + config.loserIsPartial + ); + } } } } // 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. + // For Finals (winnerFloor=null) getGuaranteedMinimumPosition returns null — the + // winner is already finalized as 1st above. For non-scoring rounds it also + // returns null (winners were given floor 5 inline above). const guaranteedMinimum = getGuaranteedMinimumPosition( round, event.bracketTemplateId, @@ -276,14 +267,14 @@ export async function processPlayoffEvent( event.sportsSeasonId, guaranteedMinimum, db, - true // isPartialScore: still competing, score will increase as they advance + true // isPartialScore: still competing, floor will rise as they advance ); } } } // Recalculate standings for all affected leagues - await recalculateAffectedLeagues(event.sportsSeasonId, db); + await recalculateAffectedLeagues(event.sportsSeasonId, db, { eventName: event.name }); // Auto-trigger probability recalculation after result try { @@ -299,6 +290,74 @@ export async function processPlayoffEvent( } } +/** + * Process a single completed match and immediately award points. + * + * Called when a match winner is set, before the full round is complete. + * Loser placement is final (isPartialScore=false); winner placement is + * provisional (isPartialScore=true) reflecting their guaranteed minimum finish. + * + * @param skipSideEffects - When true, skips standings recalc and probability update. + * Use this when processing multiple matches in a batch (call side effects once after + * the loop instead of once per match). + */ +export async function processMatchResult( + params: { + round: string; + winnerId: string; + loserId: string; + /** isScoring ?? true: default to true for legacy rows that pre-date the isScoring column. */ + isScoring: boolean; + sportsSeasonId: string; + bracketTemplateId?: string | null; + eventName?: string; + skipSideEffects?: boolean; + }, + providedDb?: ReturnType +): Promise { + const db = providedDb || database(); + const { round, winnerId, loserId, isScoring, sportsSeasonId, bracketTemplateId, eventName, skipSideEffects } = params; + + if (!isScoring) { + // Non-scoring (pre-bracket) round: loser permanently eliminated (0 pts), + // winner banks provisional T5–T8 floor immediately. + // Assumption: non-scoring round losers cannot re-enter the bracket. + await upsertParticipantResult(loserId, sportsSeasonId, 0, db); + await upsertParticipantResult(winnerId, sportsSeasonId, 5, db, true); + } else { + const config = getRoundConfig(round, bracketTemplateId); + + if (config === null) { + // Unrecognized scoring round: loser earns 0 pts, winner gets fallback T5–T8 floor. + console.warn( + `[ScoringCalculator] processMatchResult: unrecognized round "${round}". Loser receives 0 pts.` + ); + await upsertParticipantResult(loserId, sportsSeasonId, 0, db); + await upsertParticipantResult(winnerId, sportsSeasonId, 5, db, true); + } else if (config.winnerFloor === null) { + // Finals: finalize both participants. + await upsertParticipantResult(loserId, sportsSeasonId, config.loserPosition, db); + await upsertParticipantResult(winnerId, sportsSeasonId, 1, db); + } else { + // All other scoring rounds: loser gets final (or provisional) position, winner gets floor. + await upsertParticipantResult(loserId, sportsSeasonId, config.loserPosition, db, config.loserIsPartial); + await upsertParticipantResult(winnerId, sportsSeasonId, config.winnerFloor, db, true); + } + } + + if (!skipSideEffects) { + await recalculateAffectedLeagues(sportsSeasonId, db, eventName ? { eventName } : undefined); + try { + await updateProbabilitiesAfterResult(sportsSeasonId, true); + } catch (error) { + console.error( + `[ScoringCalculator] Failed to auto-update probabilities for sports season ${sportsSeasonId}:`, + error + ); + } + } +} + /** * Helper to upsert participant result * @@ -350,62 +409,17 @@ async function upsertParticipantResult( * 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) + * - The round is the Finals (winner already finalized as 1st place inline) */ 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; + const config = getRoundConfig(round, bracketTemplateId); + if (config === null) return 5; // Unknown scoring round: fallback to T5–T8 floor + return config.winnerFloor; // null for Finals; actual floor for all other rounds } /** @@ -1093,7 +1107,8 @@ export async function recalculateStandings( */ export async function recalculateAffectedLeagues( sportsSeasonId: string, - providedDb?: ReturnType + providedDb?: ReturnType, + options?: { eventName?: string } ): Promise { const db = providedDb || database(); @@ -1106,7 +1121,59 @@ export async function recalculateAffectedLeagues( // Recalculate each affected season for (const seasonId of seasonIds) { + // Capture standings before recalculation for delta calculation + const beforeStandings = await db.query.teamStandings.findMany({ + where: eq(schema.teamStandings.seasonId, seasonId), + with: { team: true }, + }); + const previousPointsById = new Map( + beforeStandings.map((s) => [s.teamId, parseFloat(s.totalPoints)]) + ); + await recalculateStandings(seasonId, db); + + // Check if any team's score changed — if so, send Discord notification + const afterStandings = await db.query.teamStandings.findMany({ + where: eq(schema.teamStandings.seasonId, seasonId), + with: { team: true }, + orderBy: (ts, { asc }) => [asc(ts.currentRank)], + }); + + const hasChanges = afterStandings.some((s) => { + const prev = previousPointsById.get(s.teamId); + return prev === undefined || prev !== parseFloat(s.totalPoints); + }); + + if (!hasChanges) continue; + + // Look up the league's Discord webhook URL via the season + const season = await db.query.seasons.findFirst({ + where: eq(schema.seasons.id, seasonId), + with: { league: true }, + }); + + const webhookUrl = season?.league?.discordWebhookUrl; + if (!webhookUrl) continue; + + const standings = afterStandings.map((s) => ({ + teamId: s.teamId, + teamName: s.team.name, + totalPoints: parseFloat(s.totalPoints), + rank: s.currentRank, + })); + + try { + await sendStandingsUpdateNotification({ + webhookUrl, + seasonName: `${season.league.name} ${season.year}`, + standings, + previousStandings: previousPointsById, + eventName: options?.eventName, + }); + } catch (err) { + // Log but don't fail the scoring pipeline if Discord is unreachable + console.error(`[ScoringCalculator] Discord notification failed for season ${seasonId}:`, err); + } } console.log( diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts index 38a3c83..d9f4b1f 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.server.ts @@ -21,7 +21,12 @@ import { upsertMatchOdds, deleteOddsForParticipant, } from "~/models/playoff-match-odds"; -import { processPlayoffEvent } from "~/models/scoring-calculator"; +import { + processPlayoffEvent, + processMatchResult, + recalculateAffectedLeagues, +} from "~/models/scoring-calculator"; +import { updateProbabilitiesAfterResult } from "~/services/probability-updater"; import { getBracketTemplate, ALL_16_SEEDS, type BracketRegion } from "~/lib/bracket-templates"; import { createGroupsForEvent, @@ -242,6 +247,19 @@ export async function action({ request, params }: Route.ActionArgs) { } } + // Immediately score this match: loser gets their final placement, + // winner gets provisional floor points (isPartialScore=true). + // isScoring ?? true: default to true for legacy rows pre-dating the isScoring column. + await processMatchResult({ + round: match.round, + winnerId, + loserId, + isScoring: match.isScoring ?? true, + sportsSeasonId: event.sportsSeasonId, + bracketTemplateId: event.bracketTemplateId, + eventName: event.name ?? undefined, + }); + return { success: "Winner set successfully" }; } catch (error) { console.error("Error setting winner:", error); @@ -294,6 +312,12 @@ export async function action({ request, params }: Route.ActionArgs) { continue; } + // Guard: ensure the match actually belongs to the submitted round. + if (match.round !== round) { + errors.push(`Match ${match.matchNumber} is in round "${match.round}", not "${round}"`); + continue; + } + // Determine loser const loserId = match.participant1Id === winnerId @@ -325,6 +349,20 @@ export async function action({ request, params }: Route.ActionArgs) { } } + // Score this match without triggering standings/probability recalc yet — + // we batch those side effects into a single call after the loop. + // isScoring ?? true: default to true for legacy rows pre-dating the isScoring column. + await processMatchResult({ + round: match.round, + winnerId, + loserId, + isScoring: match.isScoring ?? true, + sportsSeasonId: event.sportsSeasonId, + bracketTemplateId: event.bracketTemplateId, + eventName: event.name ?? undefined, + skipSideEffects: true, + }); + successCount++; } catch (error) { console.error(`Error setting winner for match ${matchId}:`, error); @@ -334,6 +372,17 @@ export async function action({ request, params }: Route.ActionArgs) { } } + // Run side effects once for the whole batch (avoids N redundant recalcs). + if (successCount > 0) { + const db = database(); + await recalculateAffectedLeagues(event.sportsSeasonId, db, event.name ? { eventName: event.name } : undefined); + try { + await updateProbabilitiesAfterResult(event.sportsSeasonId, true); + } catch (error) { + console.error(`Error updating probabilities after batch round winners:`, error); + } + } + if (errors.length > 0 && successCount === 0) { return { error: `Failed to set winners: ${errors.join(", ")}` }; } diff --git a/app/routes/leagues/__tests__/settings-update.test.ts b/app/routes/leagues/__tests__/settings-update.test.ts index 98cc214..a1cc858 100644 --- a/app/routes/leagues/__tests__/settings-update.test.ts +++ b/app/routes/leagues/__tests__/settings-update.test.ts @@ -38,6 +38,7 @@ const mockLeague = { createdBy: 'user-1', currentSeasonId: 'season-1', isPublicDraftBoard: false, + discordWebhookUrl: null, createdAt: new Date('2025-01-01'), updatedAt: new Date('2025-01-01'), }; diff --git a/app/services/discord.ts b/app/services/discord.ts new file mode 100644 index 0000000..da72893 --- /dev/null +++ b/app/services/discord.ts @@ -0,0 +1,61 @@ +interface StandingsEntry { + teamId: string; + teamName: string; + totalPoints: number; + rank: number | null; +} + +interface SendStandingsUpdateOptions { + webhookUrl: string; + seasonName: string; + standings: StandingsEntry[]; + previousStandings: Map | Record; + eventName?: string; +} + +export async function sendStandingsUpdateNotification({ + webhookUrl, + seasonName, + standings, + previousStandings, + eventName, +}: SendStandingsUpdateOptions): Promise { + const title = eventName + ? `📊 ${seasonName} — Standings after ${eventName}` + : `📊 ${seasonName} — Standings Update`; + + const getPrev = (teamId: string) => + previousStandings instanceof Map + ? previousStandings.get(teamId) + : previousStandings[teamId]; + + const lines = standings + .sort((a, b) => (a.rank ?? 999) - (b.rank ?? 999)) + .map((s) => { + const prev = getPrev(s.teamId); + const diff = + prev !== undefined ? s.totalPoints - prev : null; + const diffStr = + diff !== null && diff !== 0 + ? diff > 0 + ? ` (+${diff.toFixed(1)})` + : ` (${diff.toFixed(1)})` + : ""; + const rankStr = s.rank != null ? `${s.rank}.` : "-"; + return `${rankStr} **${s.teamName}** — ${s.totalPoints.toFixed(1)} pts${diffStr}`; + }); + + const content = [`**${title}**`, "", ...lines].join("\n"); + + const response = await fetch(webhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ content }), + }); + + if (!response.ok) { + throw new Error( + `Discord webhook returned ${response.status}: ${await response.text()}` + ); + } +} diff --git a/database/schema.ts b/database/schema.ts index c40779a..5ce457b 100644 --- a/database/schema.ts +++ b/database/schema.ts @@ -99,6 +99,7 @@ export const leagues = pgTable("leagues", { createdBy: varchar("created_by", { length: 255 }).notNull(), // Clerk user ID currentSeasonId: uuid("current_season_id"), // References the active season isPublicDraftBoard: boolean("is_public_draft_board").notNull().default(false), + discordWebhookUrl: varchar("discord_webhook_url", { length: 500 }), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), }); diff --git a/drizzle/0048_mean_enchantress.sql b/drizzle/0048_mean_enchantress.sql new file mode 100644 index 0000000..fa46e19 --- /dev/null +++ b/drizzle/0048_mean_enchantress.sql @@ -0,0 +1,19 @@ +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_enum WHERE enumlabel = 'ncaam_bracket' AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'simulator_type')) THEN + ALTER TYPE "public"."simulator_type" ADD VALUE 'ncaam_bracket'; + END IF; +END $$; +--> statement-breakpoint +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_enum WHERE enumlabel = 'ncaaw_bracket' AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'simulator_type')) THEN + ALTER TYPE "public"."simulator_type" ADD VALUE 'ncaaw_bracket'; + END IF; +END $$; +--> statement-breakpoint +DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_enum WHERE enumlabel = 'nba_bracket' AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'simulator_type')) THEN + ALTER TYPE "public"."simulator_type" ADD VALUE 'nba_bracket'; + END IF; +END $$; +--> statement-breakpoint +ALTER TABLE "leagues" ADD COLUMN IF NOT EXISTS "discord_webhook_url" varchar(500); diff --git a/drizzle/meta/0048_snapshot.json b/drizzle/meta/0048_snapshot.json new file mode 100644 index 0000000..90a66d4 --- /dev/null +++ b/drizzle/meta/0048_snapshot.json @@ -0,0 +1,3543 @@ +{ + "id": "073631b4-6a21-4bfe-b287-ec752c45fe05", + "prevId": "16019fc9-f516-4d33-86ba-c2971ca492c0", + "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 + }, + "discord_webhook_url": { + "name": "discord_webhook_url", + "type": "varchar(500)", + "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": {}, + "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_match_games": { + "name": "playoff_match_games", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "playoff_match_id": { + "name": "playoff_match_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "game_number": { + "name": "game_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "playoff_match_game_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'scheduled'" + }, + "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 + }, + "winner_id": { + "name": "winner_id", + "type": "uuid", + "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": { + "playoff_match_games_playoff_match_id_playoff_matches_id_fk": { + "name": "playoff_match_games_playoff_match_id_playoff_matches_id_fk", + "tableFrom": "playoff_match_games", + "tableTo": "playoff_matches", + "columnsFrom": [ + "playoff_match_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "playoff_match_games_winner_id_participants_id_fk": { + "name": "playoff_match_games_winner_id_participants_id_fk", + "tableFrom": "playoff_match_games", + "tableTo": "participants", + "columnsFrom": [ + "winner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.playoff_match_odds": { + "name": "playoff_match_odds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "playoff_match_id": { + "name": "playoff_match_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "participant_id": { + "name": "participant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "moneyline_odds": { + "name": "moneyline_odds", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "implied_probability": { + "name": "implied_probability", + "type": "numeric(6, 4)", + "primaryKey": false, + "notNull": false + }, + "odds_source": { + "name": "odds_source", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "recorded_at": { + "name": "recorded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "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_match_odds_playoff_match_id_playoff_matches_id_fk": { + "name": "playoff_match_odds_playoff_match_id_playoff_matches_id_fk", + "tableFrom": "playoff_match_odds", + "tableTo": "playoff_matches", + "columnsFrom": [ + "playoff_match_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "playoff_match_odds_participant_id_participants_id_fk": { + "name": "playoff_match_odds_participant_id_participants_id_fk", + "tableFrom": "playoff_match_odds", + "tableTo": "participants", + "columnsFrom": [ + "participant_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_starts_at": { + "name": "event_starts_at", + "type": "timestamp with time zone", + "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 + }, + "bracket_region_config": { + "name": "bracket_region_config", + "type": "jsonb", + "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.playoff_match_game_status": { + "name": "playoff_match_game_status", + "schema": "public", + "values": [ + "scheduled", + "complete", + "postponed" + ] + }, + "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", + "ucl_bracket", + "ncaam_bracket", + "ncaaw_bracket", + "nba_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 bb9a6fd..df3dc9c 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -337,6 +337,13 @@ "when": 1773730000000, "tag": "0047_fix_nba_bracket_simulator_type", "breakpoints": true + }, + { + "idx": 48, + "version": "7", + "when": 1773769397138, + "tag": "0048_mean_enchantress", + "breakpoints": true } ] -} +} \ No newline at end of file