Add playoff match game scheduling and odds management (#135)
* Add playoff match games and odds storage
Introduces two new tables for bracket matchup detail storage:
- `playoff_match_games`: tracks individual game schedules within a
series matchup (game number, scheduledAt, status, per-game scores,
winner). Supports scheduled/complete/postponed status enum.
- `playoff_match_odds`: stores moneyline odds per participant per
matchup (single upsert record, no isLatest complexity).
Includes:
- Drizzle schema + relations with CASCADE deletes from playoff_matches
- Migration 0040_fat_puma.sql
- playoff-match-game.ts model with pure helpers: computeSeriesScore,
isSeriesComplete, getSeriesLeader — plus full CRUD
- playoff-match-odds.ts model with pure helpers: americanToImpliedProbability,
impliedProbabilityToAmerican, normalizeOdds — plus upsert/read/delete
- findPlayoffMatchesByEventId and findPlayoffMatchById updated to
include games and odds in their query results
- Bracket server route: add-game, update-game, delete-game,
upsert-odds, delete-odds actions
- Bracket admin UI: expandable per-match panel for game schedule
management and moneyline odds entry
- 41 new unit tests (18 game + 23 odds), all 810 tests passing
https://claude.ai/code/session_01Twt3D1bsEK3eXUhMaz6ee7
* Code review fixes: type safety, abstraction, and React correctness
- Derive PlayoffMatchGameStatus from schema enum instead of hardcoding
the string union, eliminating the duplicate source of truth
- updateGame now returns PlayoffMatchGame | undefined to reflect reality
when no row matches the ID
- Remove TOCTOU check-then-act in update-game action: call updateGame
directly and check the return value instead of a pre-flight findGameById
- Add status enum validation before the cast in update-game action
- Move impliedProbability computation inside upsertMatchOdds so callers
only provide moneylineOdds; the model owns the derivation
- Remove unnecessary dynamic import of americanToImpliedProbability in
the upsert-odds action (was already imported from the same module)
- Fix React list reconciliation bug: replace bare <> fragment with
<Fragment key={match.id}> so React can correctly track rows
https://claude.ai/code/session_01Twt3D1bsEK3eXUhMaz6ee7
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 14:17:43 -07:00
|
|
|
CREATE TYPE "public"."playoff_match_game_status" AS ENUM('scheduled', 'complete', 'postponed');--> statement-breakpoint
|
2026-03-11 14:47:19 -07:00
|
|
|
DO $$ BEGIN
|
|
|
|
|
IF NOT EXISTS (
|
|
|
|
|
SELECT 1 FROM pg_enum
|
|
|
|
|
WHERE enumlabel = 'ucl_bracket'
|
|
|
|
|
AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'simulator_type')
|
|
|
|
|
) THEN
|
|
|
|
|
ALTER TYPE "public"."simulator_type" ADD VALUE 'ucl_bracket';
|
|
|
|
|
END IF;
|
|
|
|
|
END $$;--> statement-breakpoint
|
Add playoff match game scheduling and odds management (#135)
* Add playoff match games and odds storage
Introduces two new tables for bracket matchup detail storage:
- `playoff_match_games`: tracks individual game schedules within a
series matchup (game number, scheduledAt, status, per-game scores,
winner). Supports scheduled/complete/postponed status enum.
- `playoff_match_odds`: stores moneyline odds per participant per
matchup (single upsert record, no isLatest complexity).
Includes:
- Drizzle schema + relations with CASCADE deletes from playoff_matches
- Migration 0040_fat_puma.sql
- playoff-match-game.ts model with pure helpers: computeSeriesScore,
isSeriesComplete, getSeriesLeader — plus full CRUD
- playoff-match-odds.ts model with pure helpers: americanToImpliedProbability,
impliedProbabilityToAmerican, normalizeOdds — plus upsert/read/delete
- findPlayoffMatchesByEventId and findPlayoffMatchById updated to
include games and odds in their query results
- Bracket server route: add-game, update-game, delete-game,
upsert-odds, delete-odds actions
- Bracket admin UI: expandable per-match panel for game schedule
management and moneyline odds entry
- 41 new unit tests (18 game + 23 odds), all 810 tests passing
https://claude.ai/code/session_01Twt3D1bsEK3eXUhMaz6ee7
* Code review fixes: type safety, abstraction, and React correctness
- Derive PlayoffMatchGameStatus from schema enum instead of hardcoding
the string union, eliminating the duplicate source of truth
- updateGame now returns PlayoffMatchGame | undefined to reflect reality
when no row matches the ID
- Remove TOCTOU check-then-act in update-game action: call updateGame
directly and check the return value instead of a pre-flight findGameById
- Add status enum validation before the cast in update-game action
- Move impliedProbability computation inside upsertMatchOdds so callers
only provide moneylineOdds; the model owns the derivation
- Remove unnecessary dynamic import of americanToImpliedProbability in
the upsert-odds action (was already imported from the same module)
- Fix React list reconciliation bug: replace bare <> fragment with
<Fragment key={match.id}> so React can correctly track rows
https://claude.ai/code/session_01Twt3D1bsEK3eXUhMaz6ee7
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 14:17:43 -07:00
|
|
|
CREATE TABLE IF NOT EXISTS "playoff_match_games" (
|
|
|
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
|
|
|
"playoff_match_id" uuid NOT NULL,
|
|
|
|
|
"game_number" integer NOT NULL,
|
|
|
|
|
"scheduled_at" timestamp,
|
|
|
|
|
"status" "playoff_match_game_status" DEFAULT 'scheduled' NOT NULL,
|
|
|
|
|
"participant1_score" numeric(10, 2),
|
|
|
|
|
"participant2_score" numeric(10, 2),
|
|
|
|
|
"winner_id" uuid,
|
|
|
|
|
"notes" text,
|
|
|
|
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
|
|
|
"updated_at" timestamp DEFAULT now() NOT NULL
|
|
|
|
|
);
|
|
|
|
|
--> statement-breakpoint
|
|
|
|
|
CREATE TABLE IF NOT EXISTS "playoff_match_odds" (
|
|
|
|
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
|
|
|
"playoff_match_id" uuid NOT NULL,
|
|
|
|
|
"participant_id" uuid NOT NULL,
|
|
|
|
|
"moneyline_odds" integer,
|
|
|
|
|
"implied_probability" numeric(6, 4),
|
|
|
|
|
"odds_source" varchar(100),
|
|
|
|
|
"recorded_at" timestamp DEFAULT now() NOT NULL,
|
|
|
|
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
|
|
|
"updated_at" timestamp DEFAULT now() NOT NULL
|
|
|
|
|
);
|
|
|
|
|
--> statement-breakpoint
|
|
|
|
|
DO $$ BEGIN
|
|
|
|
|
ALTER TABLE "playoff_match_games" ADD CONSTRAINT "playoff_match_games_playoff_match_id_playoff_matches_id_fk" FOREIGN KEY ("playoff_match_id") REFERENCES "public"."playoff_matches"("id") ON DELETE cascade ON UPDATE no action;
|
|
|
|
|
EXCEPTION
|
|
|
|
|
WHEN duplicate_object THEN null;
|
|
|
|
|
END $$;
|
|
|
|
|
--> statement-breakpoint
|
|
|
|
|
DO $$ BEGIN
|
|
|
|
|
ALTER TABLE "playoff_match_games" ADD CONSTRAINT "playoff_match_games_winner_id_participants_id_fk" FOREIGN KEY ("winner_id") REFERENCES "public"."participants"("id") ON DELETE set null ON UPDATE no action;
|
|
|
|
|
EXCEPTION
|
|
|
|
|
WHEN duplicate_object THEN null;
|
|
|
|
|
END $$;
|
|
|
|
|
--> statement-breakpoint
|
|
|
|
|
DO $$ BEGIN
|
|
|
|
|
ALTER TABLE "playoff_match_odds" ADD CONSTRAINT "playoff_match_odds_playoff_match_id_playoff_matches_id_fk" FOREIGN KEY ("playoff_match_id") REFERENCES "public"."playoff_matches"("id") ON DELETE cascade ON UPDATE no action;
|
|
|
|
|
EXCEPTION
|
|
|
|
|
WHEN duplicate_object THEN null;
|
|
|
|
|
END $$;
|
|
|
|
|
--> statement-breakpoint
|
|
|
|
|
DO $$ BEGIN
|
|
|
|
|
ALTER TABLE "playoff_match_odds" ADD CONSTRAINT "playoff_match_odds_participant_id_participants_id_fk" FOREIGN KEY ("participant_id") REFERENCES "public"."participants"("id") ON DELETE cascade ON UPDATE no action;
|
|
|
|
|
EXCEPTION
|
|
|
|
|
WHEN duplicate_object THEN null;
|
|
|
|
|
END $$;
|