Deleting an event from a season only affects that season's scoring event; the shared canonical tournament and the other linked seasons survive. The old flow never communicated this and offered no way to manage the relationship, so it felt like deleting an event might wipe the whole shared tournament. - deleteScoringEvent is now shared-tournament-aware: it auto-heals the primary window (promotes the earliest remaining window when the primary is removed) and optionally deletes the canonical tournament when the last linked window is removed. Returns a result describing what happened. - Add deleteTournament model helper. - Events page: delete confirmation now explains exactly what a delete does (removed from this season only vs. last window), with an opt-in checkbox to also remove the orphaned shared tournament. - Tournament page: add a per-window "Remove" control on the Linked Sports Seasons card to unlink a season, reusing the auto-heal path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CBzjCLVkVaQMj1MF3K54t2
163 lines
5.2 KiB
TypeScript
163 lines
5.2 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
|
|
vi.mock("../scoring-calculator", () => ({
|
|
recalculateAffectedLeagues: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("../qualifying-points", async (importOriginal) => {
|
|
const actual = await importOriginal();
|
|
return {
|
|
...(actual as Record<string, unknown>),
|
|
recalculateParticipantQP: vi.fn(),
|
|
};
|
|
});
|
|
|
|
import { deleteScoringEvent } from "../scoring-event";
|
|
|
|
describe("deleteScoringEvent", () => {
|
|
it("decrements majorsCompleted for a processed zero-QP qualifying event", async () => {
|
|
const setCalls: unknown[] = [];
|
|
const deleteChain = { where: vi.fn().mockResolvedValue(undefined) };
|
|
const updateChain = {
|
|
set: vi.fn((values: unknown) => {
|
|
setCalls.push(values);
|
|
return { where: vi.fn().mockResolvedValue(undefined) };
|
|
}),
|
|
};
|
|
const db = {
|
|
query: {
|
|
scoringEvents: {
|
|
findFirst: vi.fn().mockResolvedValue({
|
|
id: "event-1",
|
|
sportsSeasonId: "sports-season-1",
|
|
eventType: "major_tournament",
|
|
isQualifyingEvent: true,
|
|
}),
|
|
},
|
|
eventResults: {
|
|
findMany: vi.fn().mockResolvedValue([
|
|
{
|
|
seasonParticipantId: "participant-1",
|
|
placement: 20,
|
|
qualifyingPointsAwarded: "0.00",
|
|
},
|
|
]),
|
|
},
|
|
sportsSeasons: {
|
|
findFirst: vi.fn().mockResolvedValue({
|
|
id: "sports-season-1",
|
|
majorsCompleted: 1,
|
|
}),
|
|
},
|
|
},
|
|
transaction: vi.fn(async (callback) => callback(db)),
|
|
delete: vi.fn(() => deleteChain),
|
|
update: vi.fn(() => updateChain),
|
|
} as any;
|
|
|
|
await deleteScoringEvent("event-1", db);
|
|
|
|
expect(setCalls).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ majorsCompleted: 0 }),
|
|
])
|
|
);
|
|
});
|
|
|
|
describe("shared tournament handling", () => {
|
|
// Builds a mock db for a non-qualifying schedule_event linked to a tournament,
|
|
// so the delete skips the QP/league branches and exercises only the
|
|
// shared-tournament bookkeeping.
|
|
function makeSharedDb({
|
|
deletedEvent,
|
|
remaining,
|
|
siblingTournamentId = "tournament-1",
|
|
}: {
|
|
deletedEvent: { tournamentId: string | null; isPrimary: boolean };
|
|
remaining: Array<{ id: string; isPrimary: boolean }>;
|
|
siblingTournamentId?: string;
|
|
}) {
|
|
const findFirst = vi
|
|
.fn()
|
|
// 1st call: the event being deleted
|
|
.mockResolvedValueOnce({
|
|
id: "event-1",
|
|
sportsSeasonId: "sports-season-1",
|
|
eventType: "schedule_event",
|
|
isQualifyingEvent: false,
|
|
...deletedEvent,
|
|
})
|
|
// subsequent calls: setPrimaryEvent looking up the promoted event
|
|
.mockResolvedValue({ id: "promoted", tournamentId: siblingTournamentId });
|
|
|
|
const deleteWhere = vi.fn().mockResolvedValue(undefined);
|
|
const updateWhere = vi.fn().mockResolvedValue(undefined);
|
|
const db = {
|
|
query: {
|
|
scoringEvents: {
|
|
findFirst,
|
|
findMany: vi.fn().mockResolvedValue(remaining),
|
|
},
|
|
},
|
|
transaction: vi.fn(async (callback) => callback(db)),
|
|
delete: vi.fn(() => ({ where: deleteWhere })),
|
|
update: vi.fn(() => ({ set: vi.fn(() => ({ where: updateWhere })) })),
|
|
} as any;
|
|
return { db, deleteWhere };
|
|
}
|
|
|
|
it("keeps the tournament and promotes nothing when a non-primary window is deleted", async () => {
|
|
const { db } = makeSharedDb({
|
|
deletedEvent: { tournamentId: "tournament-1", isPrimary: false },
|
|
remaining: [{ id: "event-2", isPrimary: true }],
|
|
});
|
|
|
|
const result = await deleteScoringEvent("event-1", db);
|
|
|
|
expect(result.remainingWindows).toBe(1);
|
|
expect(result.promotedPrimaryId).toBeNull();
|
|
expect(result.deletedTournament).toBe(false);
|
|
});
|
|
|
|
it("promotes the earliest remaining window when the primary window is deleted", async () => {
|
|
const { db } = makeSharedDb({
|
|
deletedEvent: { tournamentId: "tournament-1", isPrimary: true },
|
|
// findMany is ordered asc(createdAt); first is the earliest.
|
|
remaining: [
|
|
{ id: "event-2", isPrimary: false },
|
|
{ id: "event-3", isPrimary: false },
|
|
],
|
|
});
|
|
|
|
const result = await deleteScoringEvent("event-1", db);
|
|
|
|
expect(result.promotedPrimaryId).toBe("event-2");
|
|
expect(result.remainingWindows).toBe(2);
|
|
});
|
|
|
|
it("leaves the orphaned tournament intact when the last window is deleted without the flag", async () => {
|
|
const { db } = makeSharedDb({
|
|
deletedEvent: { tournamentId: "tournament-1", isPrimary: true },
|
|
remaining: [],
|
|
});
|
|
|
|
const result = await deleteScoringEvent("event-1", db);
|
|
|
|
expect(result.remainingWindows).toBe(0);
|
|
expect(result.deletedTournament).toBe(false);
|
|
});
|
|
|
|
it("deletes the orphaned tournament when the last window is deleted with the flag", async () => {
|
|
const { db } = makeSharedDb({
|
|
deletedEvent: { tournamentId: "tournament-1", isPrimary: true },
|
|
remaining: [],
|
|
});
|
|
|
|
const result = await deleteScoringEvent("event-1", db, {
|
|
deleteOrphanTournament: true,
|
|
});
|
|
|
|
expect(result.deletedTournament).toBe(true);
|
|
});
|
|
});
|
|
});
|