brackt/app/models/__tests__/standings-breakdown.test.ts

179 lines
6 KiB
TypeScript
Raw Normal View History

Record final-placement points in the score-events ledger qualifying_points (golf, tennis, CS2) and season_standings (F1) award all of their points in one step at finalization, so they produce no per-match deltas. recordMatchScoreEvents is bracket-only and fires from match processing, which meant these sports were never written to team_score_events at all and silently never appeared in Recent Scores. Add recordFinalPlacementScoreEvents, called from finalizeQualifyingPoints and processSeasonStandings. It writes one row per team at the ledger's event-level grain, carrying that team's summed award and every contributing participant, with points from calculatePickPoints so a tied golfer contributes the same split award the standings show. The row is anchored to a real scoring event rather than a null one: the event-level unique index is (teamId, seasonId, scoringEventId) and Postgres treats NULLs as distinct, so a null anchor would duplicate rows on every re-finalization instead of upserting. When no anchor can be resolved the ledger write is skipped, which leaves standings unaffected. Adds regression coverage for the two screens that had diverged — getTeamScoreBreakdown and computeCoronaStates — including an assertion that the team page's actualPoints equals calculateTeamScore's totalPoints for the same roster, and cases proving undrafted participants still count toward a tie span. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019LZLF1PAfeKdpYog3NKtyz
2026-08-07 07:38:26 +00:00
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<typeof makePick>[],
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)
});
});