diff --git a/app/models/__tests__/scoring-event-delete.test.ts b/app/models/__tests__/scoring-event-delete.test.ts index 6e29e84..81e98f7 100644 --- a/app/models/__tests__/scoring-event-delete.test.ts +++ b/app/models/__tests__/scoring-event-delete.test.ts @@ -119,6 +119,24 @@ describe("deleteScoringEvent", () => { expect(result.deletedTournament).toBe(false); }); + it("does not promote a primary when a golf-style tournament (no primary) loses a window", async () => { + // Golf-style shared majors intentionally have no primary window — every + // linked event is isPrimary=false. Deleting one must not flip a sibling + // into a primary, which would change its scoring/guard behavior. + const { db } = makeSharedDb({ + deletedEvent: { tournamentId: "tournament-1", isPrimary: false }, + remaining: [ + { id: "event-2", isPrimary: false }, + { id: "event-3", isPrimary: false }, + ], + }); + + const result = await deleteScoringEvent("event-1", db); + + expect(result.promotedPrimaryId).toBeNull(); + expect(result.remainingWindows).toBe(2); + }); + it("promotes the earliest remaining window when the primary window is deleted", async () => { const { db } = makeSharedDb({ deletedEvent: { tournamentId: "tournament-1", isPrimary: true }, diff --git a/app/models/scoring-event.ts b/app/models/scoring-event.ts index 5ba0811..fd6de2d 100644 --- a/app/models/scoring-event.ts +++ b/app/models/scoring-event.ts @@ -305,11 +305,13 @@ export async function deleteScoringEvent( remainingWindows = remaining.length; if (remaining.length > 0) { - // Auto-heal: if the deleted event was the primary window (or no sibling is - // flagged primary anymore), promote the earliest remaining window so the - // shared major stays scorable and fan-out stays deterministic. - const hasPrimary = remaining.some((e) => e.isPrimary); - if (event.isPrimary || !hasPrimary) { + // Auto-heal: if the deleted event was the primary window, promote the + // earliest remaining window so the shared major stays scorable and + // fan-out stays deterministic. Only heal when we actually removed the + // primary — golf-style majors intentionally have no primary (scored on the + // canonical tournament page), and event.isPrimary is false for them, so + // they are left untouched. + if (event.isPrimary) { await setPrimaryEvent(remaining[0].id, db); promotedPrimaryId = remaining[0].id; } @@ -1126,6 +1128,24 @@ export async function getSportsSeasonsByTournament(tournamentId: string) { }); } +/** + * Count the distinct sports-season windows linked to a tournament. Cheaper than + * getSportsSeasonsByTournament when only the count is needed (no relations). + */ +export async function countWindowsByTournament( + tournamentId: string, + providedDb?: ReturnType +): Promise { + const db = providedDb || database(); + const [row] = await db + .select({ + count: sql`count(distinct ${schema.scoringEvents.sportsSeasonId})::int`, + }) + .from(schema.scoringEvents) + .where(eq(schema.scoringEvents.tournamentId, tournamentId)); + return row?.count ?? 0; +} + /** * The primary scoring event for a tournament — the single window where the admin * builds the bracket/stages and scoring happens; its results fan out to siblings. diff --git a/app/routes/admin.sports-seasons.$id.events.server.ts b/app/routes/admin.sports-seasons.$id.events.server.ts index b284421..90b4968 100644 --- a/app/routes/admin.sports-seasons.$id.events.server.ts +++ b/app/routes/admin.sports-seasons.$id.events.server.ts @@ -8,7 +8,7 @@ import { deleteScoringEvent, bulkCreateScoringEvents, ensurePrimaryEvent, - getSportsSeasonsByTournament, + countWindowsByTournament, type CreateScoringEventData, } from "~/models/scoring-event"; import { isBracketMajor } from "~/lib/event-utils"; @@ -66,15 +66,14 @@ export async function loader({ params }: Route.LoaderArgs) { const sharedInfo = new Map(); await Promise.all( [...linkedTournamentIds].map(async (tid) => { - const [tournament, windows] = await Promise.all([ - allTournamentsForSport.find((t) => t.id === tid) - ? Promise.resolve(allTournamentsForSport.find((t) => t.id === tid)!) - : getTournamentById(tid), - getSportsSeasonsByTournament(tid), + const cached = allTournamentsForSport.find((t) => t.id === tid); + const [tournament, windowCount] = await Promise.all([ + cached ?? getTournamentById(tid), + countWindowsByTournament(tid), ]); sharedInfo.set(tid, { name: tournament?.name ?? "shared tournament", - windowCount: windows.length, + windowCount, }); }) );