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
309 lines
7.6 KiB
TypeScript
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,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|