brackt/app/models/event-result.ts
Claude ff116961f7
Address code review feedback on not-participating flag
Security: unmark-not-participating now validates the result exists, belongs
to this event, and is actually a DNP row before deleting. mark-not-participating
now returns a user-friendly error on duplicate-key constraint violations.

Code quality: extract shared getExcludedByEventMap() utility to event-result
model, eliminating the duplicated 20-line exclusion-loading block that was
copy-pasted into all three simulators. Fix hasParticipantResult() to exclude
notParticipating rows so it correctly reflects actual competition participation.
Remove optional chaining on the non-optional notParticipatingIds field in the
admin UI. Fix misleading empty-state message.

Tests: replace the misleading first tennis DNP test (which never used the
activeIds variable it created) with a test that explicitly validates the
fallback behaviour when too few players remain after exclusion. Add three CS2
DNP tests covering the excluded-team-gets-zero-QP path, the redistribution
of wins, and per-event pool independence.

https://claude.ai/code/session_01HxNPLEXzr5Km3suWrJe2F9
2026-05-19 17:53:08 +00:00

309 lines
7.6 KiB
TypeScript

import { database } from "~/database/context";
import * as schema from "~/database/schema";
import { eq, and, inArray } from "drizzle-orm";
export interface CreateEventResultData {
scoringEventId: string;
participantId: string;
placement?: number;
qualifyingPointsAwarded?: number;
eliminated?: boolean;
rawScore?: number;
notParticipating?: boolean;
}
export interface UpdateEventResultData {
placement?: number;
qualifyingPointsAwarded?: number;
eliminated?: boolean;
rawScore?: number;
notParticipating?: boolean;
}
/**
* Create an event result for a participant
*/
export async function createEventResult(
data: CreateEventResultData,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
const [result] = await db
.insert(schema.eventResults)
.values({
scoringEventId: data.scoringEventId,
seasonParticipantId: data.participantId,
placement: data.placement,
qualifyingPointsAwarded: data.qualifyingPointsAwarded?.toString(),
eliminated: data.eliminated,
rawScore: data.rawScore?.toString(),
notParticipating: data.notParticipating,
})
.returning();
return result;
}
/**
* Create multiple event results in bulk
* Useful for entering all results for a tournament at once
*/
export async function createEventResultsBulk(
results: CreateEventResultData[],
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
const inserted = await db
.insert(schema.eventResults)
.values(
results.map((r) => ({
scoringEventId: r.scoringEventId,
seasonParticipantId: r.participantId,
placement: r.placement,
qualifyingPointsAwarded: r.qualifyingPointsAwarded?.toString(),
eliminated: r.eliminated,
rawScore: r.rawScore?.toString(),
notParticipating: r.notParticipating,
}))
)
.returning();
return inserted;
}
/**
* Get an event result by ID
*/
export async function getEventResultById(
resultId: string,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
return await db.query.eventResults.findFirst({
where: eq(schema.eventResults.id, resultId),
with: {
seasonParticipant: {
with: {
sportsSeason: {
with: {
sport: true,
},
},
},
},
scoringEvent: true,
},
});
}
/**
* Get all results for a scoring event
*/
export async function getEventResults(
eventId: string,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
return await db.query.eventResults.findMany({
where: eq(schema.eventResults.scoringEventId, eventId),
orderBy: schema.eventResults.placement,
with: {
seasonParticipant: {
with: {
sportsSeason: {
with: {
sport: true,
},
},
},
},
},
});
}
/**
* Get all event results for a participant across all events
*/
export async function getParticipantEventResults(
participantId: string,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
return await db.query.eventResults.findMany({
where: eq(schema.eventResults.seasonParticipantId, participantId),
with: {
scoringEvent: true,
},
orderBy: schema.eventResults.createdAt,
});
}
/**
* Update an event result
*/
export async function updateEventResult(
resultId: string,
data: UpdateEventResultData,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
const [updated] = await db
.update(schema.eventResults)
.set({
placement: data.placement,
qualifyingPointsAwarded: data.qualifyingPointsAwarded?.toString(),
eliminated: data.eliminated,
rawScore: data.rawScore?.toString(),
notParticipating: data.notParticipating,
updatedAt: new Date(),
})
.where(eq(schema.eventResults.id, resultId))
.returning();
return updated;
}
/**
* Delete an event result
*/
export async function deleteEventResult(
resultId: string,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
await db.delete(schema.eventResults).where(eq(schema.eventResults.id, resultId));
}
/**
* Delete all results for a scoring event
*/
export async function deleteEventResults(
eventId: string,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
await db
.delete(schema.eventResults)
.where(eq(schema.eventResults.scoringEventId, eventId));
}
/**
* Check if a participant has a real result (placement/QP) for a specific event.
* Returns false for not-participating rows, which are exclusion markers, not results.
*/
export async function hasParticipantResult(
eventId: string,
participantId: string,
providedDb?: ReturnType<typeof database>
): Promise<boolean> {
const db = providedDb || database();
const result = await db.query.eventResults.findFirst({
where: and(
eq(schema.eventResults.scoringEventId, eventId),
eq(schema.eventResults.seasonParticipantId, participantId),
eq(schema.eventResults.notParticipating, false)
),
});
return !!result;
}
/**
* Build a map of eventId → Set<participantId> for all not-participating rows
* across a list of scoring events. Used by qualifying-points simulators to
* exclude withdrawn participants from each event's draw/pool in a single query.
*/
export async function getExcludedByEventMap(
eventIds: string[],
providedDb?: ReturnType<typeof database>
): Promise<Map<string, Set<string>>> {
if (eventIds.length === 0) return new Map();
const db = providedDb || database();
const rows = await db
.select({
scoringEventId: schema.eventResults.scoringEventId,
seasonParticipantId: schema.eventResults.seasonParticipantId,
})
.from(schema.eventResults)
.where(
and(
inArray(schema.eventResults.scoringEventId, eventIds),
eq(schema.eventResults.notParticipating, true)
)
);
const map = new Map<string, Set<string>>();
for (const r of rows) {
let set = map.get(r.scoringEventId);
if (!set) {
set = new Set();
map.set(r.scoringEventId, set);
}
set.add(r.seasonParticipantId);
}
return map;
}
/**
* Get all not-participating records for a scoring event.
* Used by simulators to exclude withdrawn participants from future event draws.
*/
export async function getNotParticipatingResults(
eventId: string,
providedDb?: ReturnType<typeof database>
): Promise<{ seasonParticipantId: string }[]> {
const db = providedDb || database();
return await db
.select({ seasonParticipantId: schema.eventResults.seasonParticipantId })
.from(schema.eventResults)
.where(
and(
eq(schema.eventResults.scoringEventId, eventId),
eq(schema.eventResults.notParticipating, true)
)
);
}
/**
* Get results for multiple participants in a specific event
* Useful for checking drafted participants' performance
*/
export async function getEventResultsForParticipants(
eventId: string,
participantIds: string[],
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
if (participantIds.length === 0) return [];
return await db.query.eventResults.findMany({
where: and(
eq(schema.eventResults.scoringEventId, eventId),
inArray(schema.eventResults.seasonParticipantId, participantIds)
),
with: {
seasonParticipant: {
with: {
sportsSeason: {
with: {
sport: true,
},
},
},
},
},
});
}