/** * Entry-floor scoring: points a bracket guarantees at seeding time. * * Some seedings lock in a scoring tier before a single match is played. The AFL * finals are the clearest case: a top-4 seed has the double chance, so losing the * Qualifying Final still leaves them a Semi-Final, and losing that is the 5th-6th * tier. Those teams must not sit on 0 fantasy points until their first game. */ import { describe, it, expect, vi, beforeEach } from "vitest"; interface MatchRow { round: string; participant1Id: string | null; participant2Id: string | null; } /** * Minimal db mock. applyBracketEntryFloors calls, in order: * 1. db.query.scoringEvents.findFirst → the event (for template + sportsSeasonId) * 2. db.query.playoffMatches.findMany → the bracket's match slots * 3. upsertParticipantResult per floored participant → findFirst + insert/update */ function makeDb( event: { bracketTemplateId: string | null; sportsSeasonId: string } | null, matches: MatchRow[], existingByParticipant: Record = {} ) { const insertedRows: Array> = []; const updatedRows: Array> = []; return { db: { insert: vi.fn().mockReturnValue({ values: vi.fn().mockImplementation((values: Record) => { insertedRows.push(values); return Promise.resolve(); }), }), update: vi.fn().mockReturnValue({ set: vi.fn().mockImplementation((values: Record) => { updatedRows.push(values); return { where: vi.fn().mockResolvedValue(undefined) }; }), }), query: { scoringEvents: { findFirst: vi.fn().mockResolvedValue(event) }, playoffMatches: { findMany: vi.fn().mockResolvedValue(matches) }, seasonParticipantResults: { findFirst: vi.fn().mockImplementation((args: { where?: unknown }) => { // Resolve by scanning the seeded map — the mock has no real query engine, // so tests that need an existing row use a single-participant bracket. void args; const only = Object.values(existingByParticipant)[0]; return Promise.resolve(only); }), }, }, } as never, insertedRows, updatedRows, }; } import { applyBracketEntryFloors, getBracketEntryFloor } from "../scoring-calculator"; /** The AFL bracket exactly as generateAFL10Bracket writes it: later rounds are TBD. */ const AFL_BRACKET: MatchRow[] = [ { round: "Wildcard Round", participant1Id: "seed7", participant2Id: "seed10" }, { round: "Wildcard Round", participant1Id: "seed8", participant2Id: "seed9" }, { round: "Qualifying Finals", participant1Id: "seed1", participant2Id: "seed4" }, { round: "Qualifying Finals", participant1Id: "seed2", participant2Id: "seed3" }, { round: "Elimination Finals", participant1Id: "seed5", participant2Id: null }, { round: "Elimination Finals", participant1Id: "seed6", participant2Id: null }, { round: "Semi-Finals", participant1Id: null, participant2Id: null }, { round: "Semi-Finals", participant1Id: null, participant2Id: null }, { round: "Preliminary Finals", participant1Id: null, participant2Id: null }, { round: "Preliminary Finals", participant1Id: null, participant2Id: null }, { round: "Grand Final", participant1Id: null, participant2Id: null }, ]; describe("applyBracketEntryFloors", () => { beforeEach(() => { vi.clearAllMocks(); }); describe("afl_10", () => { it("banks 5 for the top 4 and 7 for seeds 5-6, and nothing for the wildcard teams", async () => { const { db, insertedRows } = makeDb( { bracketTemplateId: "afl_10", sportsSeasonId: "ss-1" }, AFL_BRACKET ); const applied = await applyBracketEntryFloors("event-1", db); expect(applied).toBe(6); const floors = Object.fromEntries( insertedRows.map((r) => [r.participantId as string, r.finalPosition as number]) ); expect(floors).toEqual({ seed1: 5, seed2: 5, seed3: 5, seed4: 5, // double chance → 5th-6th tier seed5: 7, seed6: 7, // seeded into the Elimination Finals }); // Seeds 7-10 lose the Wildcard Round for 0, so nothing is guaranteed yet. expect(floors).not.toHaveProperty("seed7"); expect(floors).not.toHaveProperty("seed10"); }); it("writes every floor as provisional so real results supersede it", async () => { const { db, insertedRows } = makeDb( { bracketTemplateId: "afl_10", sportsSeasonId: "ss-1" }, AFL_BRACKET ); await applyBracketEntryFloors("event-1", db); expect(insertedRows.every((r) => r.isPartialScore === true)).toBe(true); expect(insertedRows.every((r) => r.sportsSeasonId === "ss-1")).toBe(true); }); it("leaves TBD slots alone — a Semi-Final nobody has reached grants nothing", async () => { const { db, insertedRows } = makeDb( { bracketTemplateId: "afl_10", sportsSeasonId: "ss-1" }, [{ round: "Semi-Finals", participant1Id: null, participant2Id: null }] ); expect(await applyBracketEntryFloors("event-1", db)).toBe(0); expect(insertedRows).toHaveLength(0); }); it("does not un-finalize a participant who already has a real result", async () => { // upsertParticipantResult's never-un-finalize guard: a finalized row must not be // dragged back to a provisional floor when the bracket is regenerated. const { db, insertedRows, updatedRows } = makeDb( { bracketTemplateId: "afl_10", sportsSeasonId: "ss-1" }, [{ round: "Qualifying Finals", participant1Id: "seed1", participant2Id: null }], { seed1: { id: "row-1", finalPosition: 1, isPartialScore: false } } ); expect(await applyBracketEntryFloors("event-1", db)).toBe(0); expect(insertedRows).toHaveLength(0); expect(updatedRows).toHaveLength(0); }); }); describe("other brackets", () => { it("is a no-op for an event with no bracket template", async () => { const { db, insertedRows } = makeDb( { bracketTemplateId: null, sportsSeasonId: "ss-1" }, AFL_BRACKET ); expect(await applyBracketEntryFloors("event-1", db)).toBe(0); expect(insertedRows).toHaveLength(0); }); it("is a no-op when the event does not exist", async () => { const { db } = makeDb(null, AFL_BRACKET); expect(await applyBracketEntryFloors("event-1", db)).toBe(0); }); it("grants nothing to an NBA bracket: every seeded round is non-scoring", async () => { const { db, insertedRows } = makeDb( { bracketTemplateId: "nba_20", sportsSeasonId: "ss-1" }, [ { round: "Play-In Round 1", participant1Id: "e7", participant2Id: "e8" }, { round: "First Round", participant1Id: "e1", participant2Id: null }, ] ); expect(await applyBracketEntryFloors("event-1", db)).toBe(0); expect(insertedRows).toHaveLength(0); }); it("grants the T5-8 tier to a simple_8 field: every entrant is already in a scoring round", async () => { const { db, insertedRows } = makeDb( { bracketTemplateId: "simple_8", sportsSeasonId: "ss-1" }, [{ round: "Quarterfinals", participant1Id: "a", participant2Id: "b" }] ); expect(await applyBracketEntryFloors("event-1", db)).toBe(2); expect(insertedRows.map((r) => r.finalPosition)).toEqual([5, 5]); }); }); }); describe("getBracketEntryFloor", () => { it("prefers a round's explicit entryFloor over its loser position", () => { // Qualifying Finals is non-scoring, so only the explicit entryFloor makes it pay. expect(getBracketEntryFloor("Qualifying Finals", "afl_10")).toBe(5); }); it("falls back to a scoring round's own loser position", () => { expect(getBracketEntryFloor("Quarterfinals", "simple_8")).toBe(5); expect(getBracketEntryFloor("Semifinals", "simple_8")).toBe(3); }); it("returns null for non-scoring rounds with no explicit floor", () => { expect(getBracketEntryFloor("Wildcard Round", "afl_10")).toBeNull(); expect(getBracketEntryFloor("First Round", "nba_20")).toBeNull(); expect(getBracketEntryFloor("Round of 64", "ncaa_68")).toBeNull(); }); it("returns null for unknown rounds and templates", () => { expect(getBracketEntryFloor("Not A Round", "afl_10")).toBeNull(); expect(getBracketEntryFloor("Quarterfinals", "not_a_template")).toBeNull(); expect(getBracketEntryFloor("Quarterfinals", null)).toBeNull(); }); });