brackt/app/models/__tests__/playoff-match-game.test.ts

131 lines
4.1 KiB
TypeScript
Raw Normal View History

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
import { describe, it, expect } from "vitest";
import {
computeSeriesScore,
isSeriesComplete,
getSeriesLeader,
} from "../playoff-match-game";
const P1 = "participant-1";
const P2 = "participant-2";
const game = (winnerId: string | null, status: "complete" | "scheduled" | "postponed" = "complete") => ({
winnerId,
status,
});
describe("computeSeriesScore", () => {
it("returns zero counts when no games are present", () => {
const result = computeSeriesScore([], P1, P2);
expect(result).toEqual({ participant1Wins: 0, participant2Wins: 0, gamesPlayed: 0 });
});
it("counts only complete games", () => {
const games = [
game(P1, "complete"),
game(null, "scheduled"),
game(P2, "postponed"),
];
const result = computeSeriesScore(games, P1, P2);
expect(result.participant1Wins).toBe(1);
expect(result.participant2Wins).toBe(0);
expect(result.gamesPlayed).toBe(1);
});
it("correctly tallies a 4-3 series", () => {
const games = [
game(P1), game(P2), game(P1), game(P2),
game(P2), game(P1), game(P1),
];
const result = computeSeriesScore(games, P1, P2);
expect(result.participant1Wins).toBe(4);
expect(result.participant2Wins).toBe(3);
expect(result.gamesPlayed).toBe(7);
});
it("ignores games won by unknown participant", () => {
const games = [game("unknown-team"), game(P1)];
const result = computeSeriesScore(games, P1, P2);
expect(result.participant1Wins).toBe(1);
expect(result.participant2Wins).toBe(0);
expect(result.gamesPlayed).toBe(2);
});
it("handles a sweep correctly", () => {
const games = [game(P1), game(P1), game(P1), game(P1)];
const result = computeSeriesScore(games, P1, P2);
expect(result.participant1Wins).toBe(4);
expect(result.participant2Wins).toBe(0);
expect(result.gamesPlayed).toBe(4);
});
});
describe("isSeriesComplete", () => {
it("returns false when no games played", () => {
expect(isSeriesComplete([], P1, P2, 4)).toBe(false);
});
it("returns false when neither team has reached required wins", () => {
const games = [game(P1), game(P1), game(P2)];
expect(isSeriesComplete(games, P1, P2, 4)).toBe(false);
});
it("returns true when participant1 reaches required wins (best of 7)", () => {
const games = [game(P1), game(P1), game(P1), game(P1)];
expect(isSeriesComplete(games, P1, P2, 4)).toBe(true);
});
it("returns true when participant2 reaches required wins", () => {
const games = [game(P2), game(P2), game(P2)];
expect(isSeriesComplete(games, P1, P2, 3)).toBe(true);
});
it("works for best of 1", () => {
expect(isSeriesComplete([game(P1)], P1, P2, 1)).toBe(true);
expect(isSeriesComplete([], P1, P2, 1)).toBe(false);
});
it("works for best of 3", () => {
const twoToZero = [game(P1), game(P1)];
expect(isSeriesComplete(twoToZero, P1, P2, 2)).toBe(true);
const oneToOne = [game(P1), game(P2)];
expect(isSeriesComplete(oneToOne, P1, P2, 2)).toBe(false);
});
it("ignores scheduled/postponed games", () => {
const games = [game(P1, "complete"), game(P1, "scheduled")];
expect(isSeriesComplete(games, P1, P2, 2)).toBe(false);
});
});
describe("getSeriesLeader", () => {
it("returns null when no games played", () => {
expect(getSeriesLeader([], P1, P2)).toBeNull();
});
it("returns null when tied", () => {
const games = [game(P1), game(P2)];
expect(getSeriesLeader(games, P1, P2)).toBeNull();
});
it("returns participant1 when they lead", () => {
const games = [game(P1), game(P1), game(P2)];
expect(getSeriesLeader(games, P1, P2)).toBe(P1);
});
it("returns participant2 when they lead", () => {
const games = [game(P2), game(P2), game(P1)];
expect(getSeriesLeader(games, P1, P2)).toBe(P2);
});
it("returns correct leader after a sweep", () => {
const games = [game(P2), game(P2), game(P2), game(P2)];
expect(getSeriesLeader(games, P1, P2)).toBe(P2);
});
it("only counts complete games", () => {
const games = [game(P2, "complete"), game(P1, "scheduled"), game(P1, "postponed")];
expect(getSeriesLeader(games, P1, P2)).toBe(P2);
});
});