/** * reprocess-bracket rebuilds a bracket's placements from scratch. What it wipes first * decides whether the clear-bracket → regenerate → reprocess repair path actually works, * and whether it takes the rest of the season's placements down with it. */ import { beforeEach, describe, expect, it, vi } from "vitest"; import { findPlayoffMatchesByEventId } from "~/models/playoff-match"; import { deleteParticipantResultsBySportsSeasonId, deleteParticipantResultsForParticipants, setParticipantResult, } from "~/models/participant-result"; import { applyBracketEntryFloors, processMatchResult, processQualifyingBracketEvent, recalculateAffectedLeagues, } from "~/models/scoring-calculator"; import { getScoringEventById } from "~/models/scoring-event"; import { findParticipantsBySportsSeasonId } from "~/models/season-participant"; import { findSportsSeasonById } from "~/models/sports-season"; import { action } from "../admin.sports-seasons.$id.events.$eventId.bracket.server"; vi.mock("~/database/context", () => ({ database: vi.fn(() => ({})) })); vi.mock("~/models/scoring-event", async (importOriginal) => ({ ...(await importOriginal()), getScoringEventById: vi.fn(), updateScoringEvent: vi.fn(), isReadOnlySibling: vi.fn(() => false), })); vi.mock("~/models/playoff-match", async (importOriginal) => ({ ...(await importOriginal()), findPlayoffMatchesByEventId: vi.fn(), })); vi.mock("~/models/participant-result", async (importOriginal) => ({ ...(await importOriginal()), deleteParticipantResultsBySportsSeasonId: vi.fn(), deleteParticipantResultsForParticipants: vi.fn(), setParticipantResult: vi.fn(), })); vi.mock("~/models/scoring-calculator", async (importOriginal) => ({ ...(await importOriginal()), applyBracketEntryFloors: vi.fn(), processMatchResult: vi.fn(), recalculateAffectedLeagues: vi.fn(), processQualifyingBracketEvent: vi.fn(), finalizeQualifyingPoints: vi.fn(), })); vi.mock("~/models/season-participant", async (importOriginal) => ({ ...(await importOriginal()), findParticipantsBySportsSeasonId: vi.fn(), })); vi.mock("~/models/sports-season", async (importOriginal) => ({ ...(await importOriginal()), findSportsSeasonById: vi.fn(), })); const params = { id: "season-1", eventId: "event-1" }; const EVENT = { id: "event-1", name: "AFL Finals", sportsSeasonId: "season-1", isQualifyingEvent: false, isPrimary: false, tournamentId: null, bracketTemplateId: "afl_10", }; function reprocessRequest(): Request { const body = new FormData(); body.set("intent", "reprocess-bracket"); return new Request("http://localhost/reprocess", { method: "POST", body }); } /** A seeded, unplayed bracket slot. */ function slot(matchNumber: number, participant1Id: string, participant2Id: string) { return { id: `m-${matchNumber}`, round: "Qualifying Finals", matchNumber, participant1Id, participant2Id, winnerId: null, loserId: null, isComplete: false, isScoring: true, }; } const run = (request: Request) => (action as unknown as (args: { request: Request; params: typeof params }) => Promise<{ error?: string; success?: string; }>)({ request, params }); function setEvent(overrides: Partial = {}) { vi.mocked(getScoringEventById).mockResolvedValue( { ...EVENT, ...overrides } as unknown as Awaited> ); } function setMatches(matches: ReturnType[]) { vi.mocked(findPlayoffMatchesByEventId).mockResolvedValue( matches as unknown as Awaited> ); } describe("reprocess-bracket", () => { beforeEach(() => { vi.clearAllMocks(); setEvent(); vi.mocked(applyBracketEntryFloors).mockResolvedValue(4); vi.mocked(findParticipantsBySportsSeasonId).mockResolvedValue( [] as unknown as Awaited> ); vi.mocked(setParticipantResult).mockResolvedValue( undefined as unknown as Awaited> ); vi.mocked(recalculateAffectedLeagues).mockResolvedValue( undefined as unknown as Awaited> ); vi.mocked(processMatchResult).mockResolvedValue( undefined as unknown as Awaited> ); vi.mocked(processQualifyingBracketEvent).mockResolvedValue( undefined as unknown as Awaited> ); vi.mocked(findSportsSeasonById).mockResolvedValue( { qualifyingPointsFinalized: false } as unknown as Awaited< ReturnType > ); }); it("clears placements even when no match has been played", async () => { // The clear-bracket → regenerate → reprocess repair path lands here: the freshly // re-seeded bracket has nothing completed, yet the discarded bracket's finalized // placements are exactly what has to go. Skipping the wipe leaves them permanently, // because upsertParticipantResult refuses to un-finalize a result. setMatches([slot(1, "p1", "p2"), slot(2, "p3", "p4")]); const result = await run(reprocessRequest()); expect(result.success).toBeDefined(); expect(deleteParticipantResultsForParticipants).toHaveBeenCalledTimes(1); const [sportsSeasonId, ids] = vi.mocked(deleteParticipantResultsForParticipants).mock.calls[0]; expect(sportsSeasonId).toBe("season-1"); expect([...ids].toSorted()).toEqual(["p1", "p2", "p3", "p4"]); }); it("scopes the wipe to this bracket, never the whole season", async () => { // A season-wide delete would take every other event's placements with it, with only // this bracket's replay able to rebuild them. setMatches([slot(1, "p1", "p2")]); await run(reprocessRequest()); expect(deleteParticipantResultsBySportsSeasonId).not.toHaveBeenCalled(); const [, ids] = vi.mocked(deleteParticipantResultsForParticipants).mock.calls[0]; expect(ids).not.toContain("p3"); }); it("passes each participant once when a team appears in more than one slot", async () => { setMatches([slot(1, "p1", "p2"), slot(2, "p1", "p3")]); await run(reprocessRequest()); const [, ids] = vi.mocked(deleteParticipantResultsForParticipants).mock.calls[0]; expect(ids).toHaveLength(3); expect([...ids].toSorted()).toEqual(["p1", "p2", "p3"]); }); it("skips empty slots rather than passing nulls through", async () => { setMatches([ { ...slot(1, "p1", "p2"), participant2Id: null as unknown as string }, ]); await run(reprocessRequest()); const [, ids] = vi.mocked(deleteParticipantResultsForParticipants).mock.calls[0]; expect(ids).toEqual(["p1"]); }); it("still takes the season-wide delete for a qualifying event", async () => { // Qualifying seasons have no legitimate per-major fantasy placements — those come // from finalizeQualifyingPoints across all majors — so that path wipes the season // on purpose and rebuilds QP from the bracket. setEvent({ isQualifyingEvent: true }); setMatches([slot(1, "p1", "p2")]); await run(reprocessRequest()); expect(deleteParticipantResultsBySportsSeasonId).toHaveBeenCalledWith("season-1", {}); expect(deleteParticipantResultsForParticipants).not.toHaveBeenCalled(); }); it("rejects an event with no bracket rather than wiping anything", async () => { setMatches([]); const result = await run(reprocessRequest()); expect(result.error).toContain("No bracket to reprocess"); expect(deleteParticipantResultsForParticipants).not.toHaveBeenCalled(); expect(deleteParticipantResultsBySportsSeasonId).not.toHaveBeenCalled(); }); });