From 80bdbbc3bb67c263bb8e42cf9bf367064ccde941 Mon Sep 17 00:00:00 2001 From: Chris Parsons <438676+chrisparsons83@users.noreply.github.com> Date: Tue, 10 Mar 2026 21:47:47 -0700 Subject: [PATCH] fix: clean up derived data when deleting scoring events (#130) - Bracket events (playoff_game): delete all participantResults for the sports season atomically with the event, then recalculate league standings - Qualifying events: capture affected participant IDs before cascade deletes eventResults, recalculate participantQualifyingTotals from remaining events, decrement majorsCompleted if QP had been processed, and recalculate league standings Co-authored-by: Claude Sonnet 4.6 --- app/models/scoring-event.ts | 63 +++++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/app/models/scoring-event.ts b/app/models/scoring-event.ts index b28281e..a9ba96b 100644 --- a/app/models/scoring-event.ts +++ b/app/models/scoring-event.ts @@ -1,6 +1,8 @@ import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { eq, and, desc, asc, gte, lte } from "drizzle-orm"; +import { recalculateAffectedLeagues } from "./scoring-calculator"; +import { recalculateParticipantQP } from "./qualifying-points"; export type EventType = "playoff_game" | "major_tournament" | "final_standings" | "schedule_event"; @@ -189,7 +191,10 @@ export async function completeScoringEvent( } /** - * Delete a scoring event + * Delete a scoring event. + * For bracket events (playoff_game), also clears all participant results for the + * sports season (placements are stored there with no FK back to the event) and + * recalculates league standings. */ export async function deleteScoringEvent( eventId: string, @@ -197,7 +202,61 @@ export async function deleteScoringEvent( ) { const db = providedDb || database(); - await db.delete(schema.scoringEvents).where(eq(schema.scoringEvents.id, eventId)); + const event = await db.query.scoringEvents.findFirst({ + where: eq(schema.scoringEvents.id, eventId), + }); + + if (!event) return; + + // For qualifying events: capture affected participant IDs before the cascade deletes + // eventResults (which is how we know who had QP awarded from this event). + let affectedParticipantIds: string[] = []; + let wasQPProcessed = false; + if (event.isQualifyingEvent) { + const results = await db.query.eventResults.findMany({ + where: eq(schema.eventResults.scoringEventId, eventId), + }); + affectedParticipantIds = results.map((r) => r.participantId); + wasQPProcessed = results.some( + (r) => r.qualifyingPointsAwarded && parseFloat(r.qualifyingPointsAwarded) > 0 + ); + } + + await db.transaction(async (tx) => { + if (event.eventType === "playoff_game") { + await tx + .delete(schema.participantResults) + .where(eq(schema.participantResults.sportsSeasonId, event.sportsSeasonId)); + } + // Cascades: playoffMatches, eventResults, tournamentGroups/Members + await tx.delete(schema.scoringEvents).where(eq(schema.scoringEvents.id, eventId)); + }); + + if (event.eventType === "playoff_game") { + await recalculateAffectedLeagues(event.sportsSeasonId, db); + } + + if (event.isQualifyingEvent && affectedParticipantIds.length > 0) { + // eventResults have been cascade-deleted; recalculate QP totals from remaining events + for (const participantId of affectedParticipantIds) { + await recalculateParticipantQP(participantId, event.sportsSeasonId, db); + } + + // Decrement majorsCompleted if this event had already been processed + if (wasQPProcessed) { + const sportsSeason = await db.query.sportsSeasons.findFirst({ + where: eq(schema.sportsSeasons.id, event.sportsSeasonId), + }); + if (sportsSeason && (sportsSeason.majorsCompleted ?? 0) > 0) { + await db + .update(schema.sportsSeasons) + .set({ majorsCompleted: (sportsSeason.majorsCompleted ?? 1) - 1, updatedAt: new Date() }) + .where(eq(schema.sportsSeasons.id, event.sportsSeasonId)); + } + } + + await recalculateAffectedLeagues(event.sportsSeasonId, db); + } } /**