import { describe, it, expect, vi, beforeEach } from "vitest"; vi.mock("~/lib/logger", () => ({ logger: { error: vi.fn(), warn: vi.fn() }, })); // ── DB mock helpers ──────────────────────────────────────────────────────── function makeInsertChain() { const onConflictDoUpdate = vi.fn().mockResolvedValue(undefined); const values = vi.fn().mockReturnValue({ onConflictDoUpdate }); const insert = vi.fn().mockReturnValue({ values }); return { insert, values, onConflictDoUpdate }; } interface MakeDbOpts { sportsSeason?: { sport: { name: string } } | null; seasonSports?: { seasonId: string }[]; picks?: { teamId: string; seasonId: string; participantId?: string }[]; scoringEvent?: { id: string; name: string } | null; participantResults?: { participantId: string; finalPosition: number | null; sportsSeasonId?: string; }[]; seasons?: { id: string; pointsFor1st: number; pointsFor2nd: number; pointsFor3rd: number; pointsFor4th: number; pointsFor5th: number; pointsFor6th: number; pointsFor7th: number; pointsFor8th: number; }[]; scoreEventRows?: { id: string; teamId: string; teamName?: string; scoringEventId: string | null; scoringEventName: string | null; sportName: string | null; pointsDelta: string; occurredAt: Date; participantIds: string[]; team: { id: string; name: string }; }[]; participantRows?: { id: string; name: string }[]; } function makeDb(opts: MakeDbOpts = {}) { const { sportsSeason = null, seasonSports = [], picks = [], seasons = [], scoreEventRows = [], participantRows = [], scoringEvent = null, participantResults = [], } = opts; const chain = makeInsertChain(); return { db: { insert: chain.insert, query: { sportsSeasons: { findFirst: vi.fn().mockResolvedValue(sportsSeason), }, seasonSports: { findMany: vi.fn().mockResolvedValue(seasonSports), }, draftPicks: { findMany: vi.fn().mockResolvedValue(picks), }, seasons: { findMany: vi.fn().mockResolvedValue(seasons), }, teamScoreEvents: { findMany: vi.fn().mockResolvedValue(scoreEventRows), }, seasonParticipants: { findMany: vi.fn().mockResolvedValue(participantRows), }, scoringEvents: { findFirst: vi.fn().mockResolvedValue(scoringEvent), }, seasonParticipantResults: { findMany: vi.fn().mockResolvedValue(participantResults), }, }, } as any, chain, }; } import { recordTeamScoreEvent, recordMatchScoreEvents, recordFinalPlacementScoreEvents, getRecentTeamScoreEvents, } from "../team-score-events"; const BASE_PARAMS = { teamId: "team-1", seasonId: "season-1", scoringEventId: "event-1", scoringEventName: "Round of 16", sportName: "Darts", participantIds: ["p-1"], pointsDelta: 10, }; describe("recordTeamScoreEvent", () => { beforeEach(() => vi.clearAllMocks()); it("uses matchId-based conflict target when matchId provided", async () => { const { db, chain } = makeDb(); await recordTeamScoreEvent({ ...BASE_PARAMS, matchId: "match-1" }, db); expect(chain.insert).toHaveBeenCalled(); expect(chain.values).toHaveBeenCalledWith( expect.objectContaining({ matchId: "match-1", pointsDelta: "10" }) ); const conflictArg = chain.onConflictDoUpdate.mock.calls[0][0]; expect(conflictArg.target).toHaveLength(3); // targetWhere should reference matchId IS NOT NULL expect(conflictArg.targetWhere).toBeDefined(); expect(conflictArg.set).toMatchObject({ pointsDelta: "10", participantIds: ["p-1"] }); }); it("uses scoringEventId-based conflict target when no matchId", async () => { const { db, chain } = makeDb(); await recordTeamScoreEvent(BASE_PARAMS, db); expect(chain.insert).toHaveBeenCalled(); expect(chain.values).toHaveBeenCalledWith( expect.objectContaining({ matchId: null }) ); const conflictArg = chain.onConflictDoUpdate.mock.calls[0][0]; expect(conflictArg.target).toHaveLength(3); expect(conflictArg.targetWhere).toBeDefined(); }); it("stores pointsDelta as string", async () => { const { db, chain } = makeDb(); await recordTeamScoreEvent({ ...BASE_PARAMS, pointsDelta: 42 }, db); expect(chain.values).toHaveBeenCalledWith(expect.objectContaining({ pointsDelta: "42" })); }); it("defaults matchId to null when not provided", async () => { const { db, chain } = makeDb(); await recordTeamScoreEvent(BASE_PARAMS, db); expect(chain.values).toHaveBeenCalledWith(expect.objectContaining({ matchId: null })); }); }); describe("recordMatchScoreEvents", () => { beforeEach(() => vi.clearAllMocks()); const BASE_EVENT = { participantId: "p-1", sportsSeasonId: "ss-1", oldFloor: 0, newFloor: 2, bracketTemplateId: null as string | null, matchId: "match-1", eventId: "event-1", eventName: "Semifinals", }; const SEASON_ROW = { id: "season-1", pointsFor1st: 100, pointsFor2nd: 75, pointsFor3rd: 50, pointsFor4th: 40, pointsFor5th: 30, pointsFor6th: 20, pointsFor7th: 10, pointsFor8th: 5, }; it("returns early when no fantasy seasons use this sports season", async () => { const { db, chain } = makeDb({ sportsSeason: { sport: { name: "Darts" } }, seasonSports: [], }); await recordMatchScoreEvents(BASE_EVENT, db); expect(chain.insert).not.toHaveBeenCalled(); }); it("returns early when no draft picks found for participant", async () => { const { db, chain } = makeDb({ sportsSeason: { sport: { name: "Darts" } }, seasonSports: [{ seasonId: "season-1" }], picks: [], seasons: [SEASON_ROW], }); await recordMatchScoreEvents(BASE_EVENT, db); expect(chain.insert).not.toHaveBeenCalled(); }); it("skips seasons where team did not draft the participant", async () => { const { db, chain } = makeDb({ sportsSeason: { sport: { name: "Darts" } }, seasonSports: [{ seasonId: "season-1" }, { seasonId: "season-2" }], picks: [{ teamId: "team-1", seasonId: "season-1" }], // only season-1 has pick seasons: [SEASON_ROW, { ...SEASON_ROW, id: "season-2" }], }); await recordMatchScoreEvents(BASE_EVENT, db); expect(chain.insert).toHaveBeenCalledTimes(1); expect(chain.values).toHaveBeenCalledWith(expect.objectContaining({ seasonId: "season-1" })); }); it("skips seasons where point delta is zero or negative", async () => { const { db, chain } = makeDb({ sportsSeason: { sport: { name: "Darts" } }, seasonSports: [{ seasonId: "season-1" }], picks: [{ teamId: "team-1", seasonId: "season-1" }], seasons: [{ ...SEASON_ROW, pointsFor1st: 0, pointsFor2nd: 0 }], // all zeros → delta=0 }); // With all-zero scoring rules, positions map to 0 points → delta = 0 → skip await recordMatchScoreEvents({ ...BASE_EVENT, oldFloor: 1, newFloor: 2 }, db); expect(chain.insert).not.toHaveBeenCalled(); }); it("records score event with correct teamId, seasonId, and matchId", async () => { const { db, chain } = makeDb({ sportsSeason: { sport: { name: "Darts" } }, seasonSports: [{ seasonId: "season-1" }], picks: [{ teamId: "team-1", seasonId: "season-1" }], seasons: [SEASON_ROW], }); await recordMatchScoreEvents(BASE_EVENT, db); expect(chain.values).toHaveBeenCalledWith( expect.objectContaining({ teamId: "team-1", seasonId: "season-1", matchId: "match-1", sportName: "Darts", participantIds: ["p-1"], }) ); }); it("uses sport name from sportsSeasons query", async () => { const { db, chain } = makeDb({ sportsSeason: { sport: { name: "Snooker" } }, seasonSports: [{ seasonId: "season-1" }], picks: [{ teamId: "team-1", seasonId: "season-1" }], seasons: [SEASON_ROW], }); await recordMatchScoreEvents(BASE_EVENT, db); expect(chain.values).toHaveBeenCalledWith( expect.objectContaining({ sportName: "Snooker" }) ); }); it("handles null sportsSeason gracefully (sportName = null)", async () => { const { db, chain } = makeDb({ sportsSeason: null, seasonSports: [{ seasonId: "season-1" }], picks: [{ teamId: "team-1", seasonId: "season-1" }], seasons: [SEASON_ROW], }); await recordMatchScoreEvents(BASE_EVENT, db); expect(chain.values).toHaveBeenCalledWith( expect.objectContaining({ sportName: null }) ); }); }); describe("getRecentTeamScoreEvents", () => { beforeEach(() => vi.clearAllMocks()); it("returns empty array when no rows found", async () => { const { db } = makeDb({ scoreEventRows: [] }); const result = await getRecentTeamScoreEvents("season-1", 10, db); expect(result).toEqual([]); }); it("does not query participants when rows array is empty", async () => { const { db } = makeDb({ scoreEventRows: [] }); await getRecentTeamScoreEvents("season-1", 10, db); expect(db.query.seasonParticipants.findMany).not.toHaveBeenCalled(); }); it("returns mapped entries with resolved participant names", async () => { const occurredAt = new Date("2024-01-15"); const { db } = makeDb({ scoreEventRows: [ { id: "tse-1", teamId: "team-1", scoringEventId: "event-1", scoringEventName: "Semifinals", sportName: "Darts", pointsDelta: "75", occurredAt, participantIds: ["p-1", "p-2"], team: { id: "team-1", name: "Team Alpha" }, }, ], participantRows: [ { id: "p-1", name: "Luke Littler" }, { id: "p-2", name: "Michael van Gerwen" }, ], }); const result = await getRecentTeamScoreEvents("season-1", 10, db); expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ id: "tse-1", teamId: "team-1", teamName: "Team Alpha", scoringEventId: "event-1", scoringEventName: "Semifinals", sportName: "Darts", pointsDelta: "75", occurredAt, }); expect(result[0].participants).toEqual([ { id: "p-1", name: "Luke Littler" }, { id: "p-2", name: "Michael van Gerwen" }, ]); }); it("omits participants whose names cannot be resolved", async () => { const { db } = makeDb({ scoreEventRows: [ { id: "tse-1", teamId: "team-1", scoringEventId: "event-1", scoringEventName: null, sportName: null, pointsDelta: "50", occurredAt: new Date(), participantIds: ["p-1", "p-missing"], team: { id: "team-1", name: "Team Beta" }, }, ], participantRows: [{ id: "p-1", name: "Known Player" }], }); const result = await getRecentTeamScoreEvents("season-1", 10, db); expect(result[0].participants).toEqual([{ id: "p-1", name: "Known Player" }]); }); it("handles null participantIds gracefully", async () => { const { db } = makeDb({ scoreEventRows: [ { id: "tse-1", teamId: "team-1", scoringEventId: "event-1", scoringEventName: null, sportName: null, pointsDelta: "10", occurredAt: new Date(), participantIds: null as any, team: { id: "team-1", name: "Team Gamma" }, }, ], participantRows: [], }); const result = await getRecentTeamScoreEvents("season-1", 10, db); expect(result[0].participants).toEqual([]); }); }); describe("recordFinalPlacementScoreEvents", () => { beforeEach(() => vi.clearAllMocks()); const RULES = { id: "season-1", pointsFor1st: 100, pointsFor2nd: 70, pointsFor3rd: 50, pointsFor4th: 40, pointsFor5th: 25, pointsFor6th: 25, pointsFor7th: 15, pointsFor8th: 15, }; function makeQpDb(overrides: Partial = {}) { return makeDb({ sportsSeason: { scoringPattern: "qualifying_points", sport: { name: "Golf" } } as any, scoringEvent: { id: "event-9", name: "The Open" }, seasonSports: [{ seasonId: "season-1" }], seasons: [RULES], ...overrides, // Tie counts are grouped by sportsSeasonId, so result rows must carry it. participantResults: (overrides.participantResults ?? []).map((r) => ({ sportsSeasonId: "ss-1", ...r, })), }); } it("writes one row per team with the summed award and its participants", async () => { const { db, chain } = makeQpDb({ participantResults: [ { participantId: "rahm", finalPosition: 8 }, { participantId: "scheffler", finalPosition: 1 }, ], picks: [ { teamId: "team-1", seasonId: "season-1", participantId: "rahm" }, { teamId: "team-1", seasonId: "season-1", participantId: "scheffler" }, ], }); await recordFinalPlacementScoreEvents( { sportsSeasonId: "ss-1", eventName: "Final Standings" }, db ); expect(chain.insert).toHaveBeenCalledTimes(1); expect(chain.values).toHaveBeenCalledWith( expect.objectContaining({ teamId: "team-1", seasonId: "season-1", scoringEventId: "event-9", scoringEventName: "Final Standings", sportName: "Golf", matchId: null, pointsDelta: "115", // 100 (1st) + 15 (8th, untied) participantIds: ["rahm", "scheffler"], }) ); }); it("awards the split value when the placement is tied", async () => { const { db, chain } = makeQpDb({ // Rahm ties for 8th with an UNDRAFTED player — the tie still halves it. participantResults: [ { participantId: "rahm", finalPosition: 8 }, { participantId: "undrafted-guy", finalPosition: 8 }, ], picks: [{ teamId: "team-1", seasonId: "season-1", participantId: "rahm" }], }); await recordFinalPlacementScoreEvents({ sportsSeasonId: "ss-1" }, db); // (15 + 0) / 2 = 7.5 → 8, matching what the standings show. expect(chain.values).toHaveBeenCalledWith( expect.objectContaining({ pointsDelta: "8", participantIds: ["rahm"] }) ); }); it("keeps each team's award separate", async () => { const { db, chain } = makeQpDb({ participantResults: [ { participantId: "rahm", finalPosition: 8 }, { participantId: "scheffler", finalPosition: 1 }, ], picks: [ { teamId: "team-1", seasonId: "season-1", participantId: "rahm" }, { teamId: "team-2", seasonId: "season-1", participantId: "scheffler" }, ], }); await recordFinalPlacementScoreEvents({ sportsSeasonId: "ss-1" }, db); expect(chain.insert).toHaveBeenCalledTimes(2); const deltasByTeam = Object.fromEntries( chain.values.mock.calls.map(([v]: any[]) => [v.teamId, v.pointsDelta]) ); expect(deltasByTeam).toEqual({ "team-1": "15", "team-2": "100" }); }); it("upserts on the event-level target so re-finalizing does not duplicate", async () => { const { db, chain } = makeQpDb({ participantResults: [{ participantId: "rahm", finalPosition: 8 }], picks: [{ teamId: "team-1", seasonId: "season-1", participantId: "rahm" }], }); await recordFinalPlacementScoreEvents({ sportsSeasonId: "ss-1" }, db); const conflictArg = chain.onConflictDoUpdate.mock.calls[0][0]; expect(conflictArg.target).toHaveLength(3); expect(conflictArg.targetWhere).toBeDefined(); expect(conflictArg.set).toMatchObject({ pointsDelta: "15" }); }); it("skips the write when no scoring event can anchor the row", async () => { // A null scoringEventId would defeat the partial unique index, since // Postgres treats NULLs as distinct — better to skip than duplicate. const { db, chain } = makeQpDb({ scoringEvent: null, participantResults: [{ participantId: "rahm", finalPosition: 8 }], picks: [{ teamId: "team-1", seasonId: "season-1", participantId: "rahm" }], }); await recordFinalPlacementScoreEvents({ sportsSeasonId: "ss-1" }, db); expect(chain.insert).not.toHaveBeenCalled(); }); it("ignores non-scoring placements", async () => { const { db, chain } = makeQpDb({ participantResults: [ { participantId: "rahm", finalPosition: 0 }, { participantId: "other", finalPosition: null }, ], picks: [{ teamId: "team-1", seasonId: "season-1", participantId: "rahm" }], }); await recordFinalPlacementScoreEvents({ sportsSeasonId: "ss-1" }, db); expect(chain.insert).not.toHaveBeenCalled(); }); it("scores season_standings placements with the shared tier averaging", async () => { const { db, chain } = makeQpDb({ sportsSeason: { scoringPattern: "season_standings", sport: { name: "F1" } } as any, participantResults: [{ participantId: "driver", finalPosition: 3 }], picks: [{ teamId: "team-1", seasonId: "season-1", participantId: "driver" }], }); await recordFinalPlacementScoreEvents({ sportsSeasonId: "ss-1" }, db); expect(chain.values).toHaveBeenCalledWith( expect.objectContaining({ sportName: "F1", pointsDelta: "50" }) ); }); });