brackt/app/models/surface-elo.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

129 lines
4.2 KiB
TypeScript

/**
* Model for Participant Surface Elos
*
* Manages surface-specific Elo ratings for tennis (and future surface-based sports).
* Each participant in a sports season can have separate Elo ratings for hard,
* clay, and grass courts.
*/
import { database } from "~/database/context";
import { seasonParticipantSurfaceElos, seasonParticipants } from "~/database/schema";
import { eq, sql } from "drizzle-orm";
export type CourtSurface = "hard" | "clay" | "grass";
export interface SurfaceEloRecord {
id: string;
participantId: string;
sportsSeasonId: string;
worldRanking: number | null;
eloHard: number | null;
eloClay: number | null;
eloGrass: number | null;
updatedAt: Date;
}
export interface SurfaceEloWithName extends SurfaceEloRecord {
participantName: string;
}
export interface SurfaceEloInput {
participantId: string;
sportsSeasonId: string;
worldRanking?: number | null;
eloHard?: number | null;
eloClay?: number | null;
eloGrass?: number | null;
}
/**
* Get all surface Elo records for a sports season, joined with participant names.
* Returns one record per participant (seasonParticipants with no Elo record are excluded).
*/
export async function getSurfaceElosForSeason(
sportsSeasonId: string
): Promise<SurfaceEloWithName[]> {
const db = database();
const rows = await db
.select({
id: seasonParticipantSurfaceElos.id,
participantId: seasonParticipantSurfaceElos.participantId,
sportsSeasonId: seasonParticipantSurfaceElos.sportsSeasonId,
worldRanking: seasonParticipantSurfaceElos.worldRanking,
eloHard: seasonParticipantSurfaceElos.eloHard,
eloClay: seasonParticipantSurfaceElos.eloClay,
eloGrass: seasonParticipantSurfaceElos.eloGrass,
updatedAt: seasonParticipantSurfaceElos.updatedAt,
participantName: seasonParticipants.name,
})
.from(seasonParticipantSurfaceElos)
.innerJoin(seasonParticipants, eq(seasonParticipantSurfaceElos.participantId, seasonParticipants.id))
.where(eq(seasonParticipantSurfaceElos.sportsSeasonId, sportsSeasonId))
.orderBy(seasonParticipants.name);
return rows;
}
/**
* Upsert surface Elo ratings for a batch of seasonParticipants.
* Uses INSERT ... ON CONFLICT DO UPDATE so all three surface columns are
* overwritten atomically — the admin always submits all three values.
*/
export async function batchUpsertSurfaceElos(
inputs: SurfaceEloInput[]
): Promise<void> {
if (inputs.length === 0) return;
const db = database();
const now = new Date();
await db
.insert(seasonParticipantSurfaceElos)
.values(
inputs.map(({ participantId, sportsSeasonId, worldRanking, eloHard, eloClay, eloGrass }) => ({
participantId,
sportsSeasonId,
worldRanking: worldRanking ?? null,
eloHard: eloHard ?? null,
eloClay: eloClay ?? null,
eloGrass: eloGrass ?? null,
updatedAt: now,
}))
)
.onConflictDoUpdate({
target: [seasonParticipantSurfaceElos.participantId, seasonParticipantSurfaceElos.sportsSeasonId],
set: {
worldRanking: sql`excluded.world_ranking`,
eloHard: sql`excluded.elo_hard`,
eloClay: sql`excluded.elo_clay`,
eloGrass: sql`excluded.elo_grass`,
updatedAt: sql`excluded.updated_at`,
},
});
}
/**
* Returns a Map from participantId to surface Elos for use in the simulator.
* Participants with no record are absent from the map (simulator falls back to 1500).
*/
export async function getSurfaceEloMap(
sportsSeasonId: string
): Promise<Map<string, { worldRanking: number | null; eloHard: number | null; eloClay: number | null; eloGrass: number | null }>> {
const db = database();
const rows = await db
.select({
participantId: seasonParticipantSurfaceElos.participantId,
worldRanking: seasonParticipantSurfaceElos.worldRanking,
eloHard: seasonParticipantSurfaceElos.eloHard,
eloClay: seasonParticipantSurfaceElos.eloClay,
eloGrass: seasonParticipantSurfaceElos.eloGrass,
})
.from(seasonParticipantSurfaceElos)
.where(eq(seasonParticipantSurfaceElos.sportsSeasonId, sportsSeasonId));
return new Map(rows.map((r) => [r.participantId, {
worldRanking: r.worldRanking,
eloHard: r.eloHard,
eloClay: r.eloClay,
eloGrass: r.eloGrass,
}]));
}