/** * 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 { participantSurfaceElos, 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 { 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 { 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). */ /** * Build a map keyed by seasonParticipant.id → surface Elo values, sourced from * the canonical `participant_surface_elos` table. Joins seasonParticipants → * canonical participants → canonical surfaceElo. * * Season participants without a linked canonical participant, or with no * canonical surface Elo row, are simply absent from the map — callers should * handle `undefined` for such cases (as the tennis simulator does). */ export async function getSurfaceEloMap( sportsSeasonId: string ): Promise> { const db = database(); const rows = await db .select({ seasonParticipantId: seasonParticipants.id, worldRanking: participantSurfaceElos.worldRanking, eloHard: participantSurfaceElos.eloHard, eloClay: participantSurfaceElos.eloClay, eloGrass: participantSurfaceElos.eloGrass, }) .from(seasonParticipants) .innerJoin( participantSurfaceElos, eq(participantSurfaceElos.participantId, seasonParticipants.participantId), ) .where(eq(seasonParticipants.sportsSeasonId, sportsSeasonId)); return new Map(rows.map((r) => [r.seasonParticipantId, { worldRanking: r.worldRanking, eloHard: r.eloHard, eloClay: r.eloClay, eloGrass: r.eloGrass, }])); }