import { describe, it, expect, vi, beforeEach } from "vitest"; /** * Regression tests for getTeamScoreBreakdown — the team detail page's points. * * This path used to branch only on `isBracket`, so a qualifying_points * participant tied for a placement was awarded the FULL placement value here * while calculateTeamScore (which writes team_standings) awarded the split. A * team therefore read 225 on its team page and 218 in the standings. * * The load-bearing assertion is the last one: the breakdown's actualPoints must * equal calculateTeamScore's totalPoints for the same fixture. Anything that * makes these two disagree reintroduces the bug. */ vi.mock("~/services/ev-calculator", () => ({ calculateEV: vi.fn(), })); vi.mock("../participant-expected-value", () => ({ getParticipantEV: vi.fn(), })); import { getTeamScoreBreakdown } from "../standings"; import { calculateTeamScore } from "../scoring-calculator"; import { getParticipantEV } from "../participant-expected-value"; const SCORING = { pointsFor1st: 100, pointsFor2nd: 70, pointsFor3rd: 50, pointsFor4th: 40, pointsFor5th: 25, pointsFor6th: 25, pointsFor7th: 15, pointsFor8th: 15, }; function makePick( participantId: string, name: string, scoringPattern: string, finalPosition: number | null, { sportsSeasonId = "ss-1", isPartialScore = false, pickNumber = 1 } = {} ) { return { pickNumber, round: 1, participant: { id: participantId, name, sportsSeasonId, results: finalPosition === null ? [] : [{ finalPosition, isPartialScore }], sportsSeason: { scoringPattern, sport: { name: "Golf" } }, }, }; } /** * @param extraResults results for participants NOBODY drafted. They still count * toward a tie span, which is exactly why the tie query cannot be narrowed to * drafted participants. */ function makeDb( picks: ReturnType[], extraResults: { sportsSeasonId: string; finalPosition: number }[] = [] ) { const seasonResults = [ ...picks.flatMap((pick) => pick.participant.results .filter((r) => r.finalPosition !== null) .map((r) => ({ sportsSeasonId: pick.participant.sportsSeasonId, finalPosition: r.finalPosition, })) ), ...extraResults, ]; return { query: { seasons: { findFirst: vi.fn().mockResolvedValue(SCORING) }, draftPicks: { findMany: vi.fn().mockResolvedValue(picks) }, scoringEvents: { findFirst: vi.fn().mockResolvedValue({ bracketTemplateId: null }) }, seasonParticipantResults: { findMany: vi.fn().mockResolvedValue(seasonResults) }, teams: { findFirst: vi.fn().mockResolvedValue({ id: "team-1", name: "Mike" }) }, }, } as any; } describe("getTeamScoreBreakdown", () => { beforeEach(() => { vi.clearAllMocks(); vi.mocked(getParticipantEV).mockResolvedValue(null as any); }); it("splits a tied qualifying_points placement", async () => { // Rahm tied for 8th with an undrafted player: (15 + 0) / 2 = 7.5 → 8. const db = makeDb( [makePick("rahm", "Jon Rahm", "qualifying_points", 8)], [{ sportsSeasonId: "ss-1", finalPosition: 8 }] ); const breakdown = await getTeamScoreBreakdown("team-1", "season-1", db); expect(breakdown?.picks[0]).toMatchObject({ participant: expect.objectContaining({ name: "Jon Rahm" }), finalPosition: 8, points: 8, }); expect(breakdown?.actualPoints).toBe(8); }); it("awards the full placement value when untied", async () => { const db = makeDb([makePick("rahm", "Jon Rahm", "qualifying_points", 8)]); const breakdown = await getTeamScoreBreakdown("team-1", "season-1", db); expect(breakdown?.picks[0].points).toBe(15); expect(breakdown?.actualPoints).toBe(15); }); it("counts undrafted participants in the tie span", async () => { // Three-way tie for 6th where two of the three went undrafted: // (25 + 15 + 15) / 3 = 18.33 → 18, the rules page's published example. const db = makeDb( [makePick("golfer", "Tied Golfer", "qualifying_points", 6)], [ { sportsSeasonId: "ss-1", finalPosition: 6 }, { sportsSeasonId: "ss-1", finalPosition: 6 }, ] ); const breakdown = await getTeamScoreBreakdown("team-1", "season-1", db); expect(breakdown?.picks[0].points).toBe(18); }); it("still averages bracket tiers", async () => { const db = makeDb([makePick("team-a", "Team A", "playoff_bracket", 5)]); const breakdown = await getTeamScoreBreakdown("team-1", "season-1", db); // Four QF losers share 5th-8th: (25 + 25 + 15 + 15) / 4 = 20 expect(breakdown?.picks[0].points).toBe(20); }); it("leaves picks without a result at zero points", async () => { const db = makeDb([makePick("pending", "Pending Golfer", "qualifying_points", null)]); const breakdown = await getTeamScoreBreakdown("team-1", "season-1", db); expect(breakdown?.picks[0]).toMatchObject({ points: 0, isComplete: false }); expect(breakdown?.actualPoints).toBe(0); }); it("agrees with calculateTeamScore on the same roster", async () => { // The reported bug in miniature: a tied golfer plus untied picks. Both // functions read the same fixture, so any divergence is a real regression. const roster = () => [ makePick("rahm", "Jon Rahm", "qualifying_points", 8, { pickNumber: 1 }), makePick("scheffler", "Scottie Scheffler", "qualifying_points", 1, { pickNumber: 2 }), makePick("team-a", "Team A", "playoff_bracket", 5, { sportsSeasonId: "ss-2", pickNumber: 3, }), ]; const undrafted = [{ sportsSeasonId: "ss-1", finalPosition: 8 }]; const breakdown = await getTeamScoreBreakdown( "team-1", "season-1", makeDb(roster(), undrafted) ); const score = await calculateTeamScore( "team-1", "season-1", makeDb(roster(), undrafted) ); expect(breakdown?.actualPoints).toBe(score.totalPoints); expect(breakdown?.actualPoints).toBe(128); // 8 (T8) + 100 (1st) + 20 (T5-8) }); });