brackt/app/models/__tests__/team-score-events.test.ts
Claude 75798d9711 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-23 01:57:54 +00:00

531 lines
17 KiB
TypeScript

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<MakeDbOpts> = {}) {
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" })
);
});
});