From 335d1bf0156087c6a9e1971d742374eaf186e0de Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Sun, 12 Apr 2026 14:28:07 -0700 Subject: [PATCH] feat: fill 0-QP rows for unplaced participants on qualifying event finalize Co-Authored-By: Claude Sonnet 4.6 --- .../__tests__/batch-results-helpers.test.ts | 21 ++++++++++++++++- ...rts-seasons.$id.events.$eventId.helpers.ts | 12 ++++++++++ ...orts-seasons.$id.events.$eventId.server.ts | 23 +++++++++++++++++-- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/app/routes/__tests__/batch-results-helpers.test.ts b/app/routes/__tests__/batch-results-helpers.test.ts index a40172e..a67bdc1 100644 --- a/app/routes/__tests__/batch-results-helpers.test.ts +++ b/app/routes/__tests__/batch-results-helpers.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { filterNewResults } from "../admin.sports-seasons.$id.events.$eventId.helpers"; +import { filterNewResults, findParticipantsWithoutResults } from "../admin.sports-seasons.$id.events.$eventId.helpers"; describe("filterNewResults", () => { it("returns all rows when no existing results", () => { @@ -27,3 +27,22 @@ describe("filterNewResults", () => { expect(filterNewResults(incoming, existing)).toEqual([]); }); }); + +describe("findParticipantsWithoutResults", () => { + it("returns participantIds not in existingResultIds", () => { + const allIds = ["p1", "p2", "p3"]; + const existingIds = new Set(["p1"]); + expect(findParticipantsWithoutResults(allIds, existingIds)).toEqual(["p2", "p3"]); + }); + + it("returns empty array when all participants have results", () => { + const allIds = ["p1", "p2"]; + const existingIds = new Set(["p1", "p2"]); + expect(findParticipantsWithoutResults(allIds, existingIds)).toEqual([]); + }); + + it("returns all when none have results", () => { + const allIds = ["p1", "p2"]; + expect(findParticipantsWithoutResults(allIds, new Set())).toEqual(["p1", "p2"]); + }); +}); diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.helpers.ts b/app/routes/admin.sports-seasons.$id.events.$eventId.helpers.ts index 1f449ca..8c65e39 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.helpers.ts +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.helpers.ts @@ -1,3 +1,15 @@ +/** + * Return participantIds from allParticipantIds that have no entry + * in existingResultParticipantIds. Used to find participants who + * need a 0-QP row after a qualifying event is finalized. + */ +export function findParticipantsWithoutResults( + allParticipantIds: string[], + existingResultParticipantIds: Set +): string[] { + return allParticipantIds.filter((id) => !existingResultParticipantIds.has(id)); +} + /** * Filter incoming batch results to only those whose participantId * is not already in the set of existing result participantIds. diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.server.ts b/app/routes/admin.sports-seasons.$id.events.$eventId.server.ts index 1288c0d..081ed8e 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.server.ts +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.server.ts @@ -16,7 +16,7 @@ import { type CreateEventResultData, type UpdateEventResultData, } from "~/models/event-result"; -import { filterNewResults } from "./admin.sports-seasons.$id.events.$eventId.helpers"; +import { filterNewResults, findParticipantsWithoutResults } from "./admin.sports-seasons.$id.events.$eventId.helpers"; import { findParticipantResultsBySportsSeasonId } from "~/models/participant-result"; import { upsertParticipantSeasonResult, @@ -126,9 +126,28 @@ export async function action({ request, params }: Route.ActionArgs) { return { success: "Event completed and fantasy placements assigned to top 8!" }; } - // If this is a qualifying event, process qualifying points + // If this is a qualifying event, process qualifying points then fill 0-QP rows if (event.isQualifyingEvent) { await processQualifyingEvent(params.eventId); + + // Write 0-QP rows for all season participants who have no result for this event. + // This marks them as "competed, earned nothing" so simulations skip this event. + const allParticipants = await findParticipantsBySportsSeasonId(params.id); + const existingResults = await getEventResults(params.eventId); + const existingIds = new Set(existingResults.map((r) => r.participantId)); + const missingIds = findParticipantsWithoutResults( + allParticipants.map((p) => p.id), + existingIds + ); + + for (const participantId of missingIds) { + await createEventResult({ + scoringEventId: params.eventId, + participantId, + qualifyingPointsAwarded: 0, + }); + } + return { success: "Event completed and qualifying points awarded!" }; }