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 <noreply@anthropic.com>
This commit is contained in:
Chris Parsons 2026-03-10 21:47:47 -07:00 committed by GitHub
parent 50722531ca
commit 80bdbbc3bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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);
}
}
/**