brackt/app/models/qualifying-points.ts
Chris Parsons fd99cab61b
refactor(models): update model layer to use renamed schema exports
Updated all model files to use the renamed schema exports from Task 1:
- participants → seasonParticipants
- participantExpectedValues → seasonParticipantExpectedValues
- participantQualifyingTotals → seasonParticipantQualifyingTotals
- participantResults → seasonParticipantResults
- participantSurfaceElos → seasonParticipantSurfaceElos
- eventResults.participantId → eventResults.seasonParticipantId
- db.query relation accessors updated
- Relation field .participant → .seasonParticipant where applicable
- Import paths updated: ./participant → ./season-participant

Files updated (14 model files + 3 test files):
- draft-pick.ts
- draft-utils.ts
- event-result.ts
- group-stage-match.ts
- participant-result.ts
- qualifying-points.ts
- scoring-calculator.ts
- scoring-event.ts
- sports-season.ts
- surface-elo.ts
- team-score-events.ts
- cs2-major-stage.ts
- golf-skills.ts
- participant-expected-value.ts
- __tests__/sports-season.clone.test.ts
- __tests__/auto-pick.test.ts
- __tests__/executeAutoPick.timer.test.ts

Typecheck errors decreased: 779 → 499 (280 fewer)
All model file errors related to renamed schemas resolved.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 06:37:53 +00:00

348 lines
9.4 KiB
TypeScript

import { database } from "~/database/context";
import * as schema from "~/database/schema";
import { eq, and, desc, sql } from "drizzle-orm";
/**
* Default qualifying point values (as specified in scoring-system.md)
*/
export const DEFAULT_QP_VALUES = [
{ placement: 1, points: 20 },
{ placement: 2, points: 14 },
{ placement: 3, points: 10 },
{ placement: 4, points: 8 },
{ placement: 5, points: 5 },
{ placement: 6, points: 5 },
{ placement: 7, points: 3 },
{ placement: 8, points: 3 },
{ placement: 9, points: 2 },
{ placement: 10, points: 2 },
{ placement: 11, points: 2 },
{ placement: 12, points: 2 },
{ placement: 13, points: 1 },
{ placement: 14, points: 1 },
{ placement: 15, points: 1 },
{ placement: 16, points: 1 },
];
export interface QualifyingPointConfigData {
placement: number;
points: number;
}
/**
* Initialize default qualifying point configuration for a sports season
*/
export async function initializeQPConfig(
sportsSeasonId: string,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
// Check if config already exists
const existing = await db.query.qualifyingPointConfig.findMany({
where: eq(schema.qualifyingPointConfig.sportsSeasonId, sportsSeasonId),
});
if (existing.length > 0) {
return existing;
}
// Insert default config
const configs = await db
.insert(schema.qualifyingPointConfig)
.values(
DEFAULT_QP_VALUES.map((qp) => ({
sportsSeasonId,
placement: qp.placement,
points: qp.points.toString(),
}))
)
.returning();
return configs;
}
/**
* Get qualifying point configuration for a sports season
*/
export async function getQPConfig(
sportsSeasonId: string,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
const configs = await db.query.qualifyingPointConfig.findMany({
where: eq(schema.qualifyingPointConfig.sportsSeasonId, sportsSeasonId),
orderBy: [schema.qualifyingPointConfig.placement],
});
// If no config exists, initialize with defaults
if (configs.length === 0) {
return await initializeQPConfig(sportsSeasonId, db);
}
return configs;
}
/**
* Update qualifying point configuration for a sports season
*/
export async function updateQPConfig(
sportsSeasonId: string,
configData: QualifyingPointConfigData[],
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
// Delete existing config
await db
.delete(schema.qualifyingPointConfig)
.where(eq(schema.qualifyingPointConfig.sportsSeasonId, sportsSeasonId));
// Insert new config
const configs = await db
.insert(schema.qualifyingPointConfig)
.values(
configData.map((qp) => ({
sportsSeasonId,
placement: qp.placement,
points: qp.points.toString(),
}))
)
.returning();
return configs;
}
/**
* Get qualifying points awarded for a specific placement
*/
export async function getQPForPlacement(
sportsSeasonId: string,
placement: number,
providedDb?: ReturnType<typeof database>
): Promise<number> {
const db = providedDb || database();
const config = await db.query.qualifyingPointConfig.findFirst({
where: and(
eq(schema.qualifyingPointConfig.sportsSeasonId, sportsSeasonId),
eq(schema.qualifyingPointConfig.placement, placement)
),
});
return config ? parseFloat(config.points) : 0;
}
/**
* Get or create participant qualifying total record
*/
export async function getOrCreateParticipantQPTotal(
participantId: string,
sportsSeasonId: string,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
let total = await db.query.seasonParticipantQualifyingTotals.findFirst({
where: and(
eq(schema.seasonParticipantQualifyingTotals.participantId, participantId),
eq(schema.seasonParticipantQualifyingTotals.sportsSeasonId, sportsSeasonId)
),
});
if (!total) {
const [created] = await db
.insert(schema.seasonParticipantQualifyingTotals)
.values({
participantId,
sportsSeasonId,
totalQualifyingPoints: "0",
eventsScored: 0,
})
.returning();
total = created;
}
return total;
}
/**
* Add qualifying points to a participant's total
* NOTE: eventsScored is NOT updated here - it should be recalculated after processing events
*/
export async function addQualifyingPoints(
participantId: string,
sportsSeasonId: string,
pointsToAdd: number,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
// Get or create the total record
const total = await getOrCreateParticipantQPTotal(participantId, sportsSeasonId, db);
// Update with new points (do NOT increment eventsScored here)
const [updated] = await db
.update(schema.seasonParticipantQualifyingTotals)
.set({
totalQualifyingPoints: (parseFloat(total.totalQualifyingPoints) + pointsToAdd).toString(),
updatedAt: new Date(),
})
.where(eq(schema.seasonParticipantQualifyingTotals.id, total.id))
.returning();
return updated;
}
/**
* Recalculate total QP and eventsScored for a participant from scratch
* This is the source of truth - sums all event_results with QP awarded
*/
export async function recalculateParticipantQP(
participantId: string,
sportsSeasonId: string,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
// Get all event results for this participant in this sports season
const eventResults = await db.query.eventResults.findMany({
where: eq(schema.eventResults.seasonParticipantId, participantId),
with: {
scoringEvent: true,
},
});
// Calculate totals from event_results
let totalQP = 0;
const uniqueEvents = new Set<string>();
for (const result of eventResults) {
if (
result.scoringEvent.sportsSeasonId === sportsSeasonId &&
result.qualifyingPointsAwarded
) {
const qp = parseFloat(result.qualifyingPointsAwarded);
if (qp > 0) {
totalQP += qp;
uniqueEvents.add(result.scoringEvent.id);
}
}
}
const eventsScored = uniqueEvents.size;
// Get or create the participant's QP total record
const total = await getOrCreateParticipantQPTotal(participantId, sportsSeasonId, db);
// Update with recalculated values
await db
.update(schema.seasonParticipantQualifyingTotals)
.set({
totalQualifyingPoints: totalQP.toString(),
eventsScored,
updatedAt: new Date(),
})
.where(eq(schema.seasonParticipantQualifyingTotals.id, total.id));
return { totalQP, eventsScored };
}
/**
* Recalculate eventsScored for a participant by counting unique events with QP awarded
* @deprecated Use recalculateParticipantQP instead for more accurate recalculation
*/
export async function recalculateEventsScored(
participantId: string,
sportsSeasonId: string,
providedDb?: ReturnType<typeof database>
) {
const result = await recalculateParticipantQP(participantId, sportsSeasonId, providedDb);
return result.eventsScored;
}
/**
* Get all qualifying totals for a sports season, ordered by points (highest first)
*/
export async function getQPStandings(
sportsSeasonId: string,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
return await db.query.seasonParticipantQualifyingTotals.findMany({
where: eq(schema.seasonParticipantQualifyingTotals.sportsSeasonId, sportsSeasonId),
orderBy: [desc(sql`CAST(${schema.seasonParticipantQualifyingTotals.totalQualifyingPoints} AS DECIMAL)`)],
with: {
participant: true,
},
});
}
/**
* Update final rankings for qualifying point standings (after finalization)
*/
export async function updateFinalRankings(
sportsSeasonId: string,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
const standings = await getQPStandings(sportsSeasonId, db);
// Group participants by QP total to handle ties
const groupedByPoints = new Map<string, typeof standings>();
for (const standing of standings) {
const points = standing.totalQualifyingPoints;
if (!groupedByPoints.has(points)) {
groupedByPoints.set(points, []);
}
groupedByPoints.get(points)?.push(standing);
}
// Assign rankings, handling ties
let currentRank = 1;
const updates = [];
// Sort by points descending
const sortedGroups = Array.from(groupedByPoints.entries()).toSorted((a, b) => {
return parseFloat(b[0]) - parseFloat(a[0]);
});
for (const [_, group] of sortedGroups) {
// All participants in this group get the same rank
for (const standing of group) {
updates.push({
id: standing.id,
finalRanking: currentRank,
});
}
// Increment rank by the size of the group
currentRank += group.length;
}
// Update all rankings
for (const update of updates) {
await db
.update(schema.seasonParticipantQualifyingTotals)
.set({ finalRanking: update.finalRanking, updatedAt: new Date() })
.where(eq(schema.seasonParticipantQualifyingTotals.id, update.id));
}
return updates;
}
/**
* Reset qualifying points for a sports season (useful for testing or corrections)
*/
export async function resetQualifyingPoints(
sportsSeasonId: string,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
await db
.delete(schema.seasonParticipantQualifyingTotals)
.where(eq(schema.seasonParticipantQualifyingTotals.sportsSeasonId, sportsSeasonId));
}