/** * Model for Participant Golf Skills * * Manages golf-specific skill data for qualifying-points golf seasons. * Primary metric: SG: Total (strokes gained per round vs. field average). * Optional per-major American odds allow major-specific probability blending. */ import { database } from "~/database/context"; import { participantGolfSkills, seasonParticipants } from "~/database/schema"; import { eq, sql } from "drizzle-orm"; export interface GolfSkillsRecord { id: string; participantId: string; sportsSeasonId: string; sgTotal: number | null; datagolfRank: number | null; mastersOdds: number | null; usOpenOdds: number | null; openChampionshipOdds: number | null; pgaChampionshipOdds: number | null; updatedAt: Date; } export interface GolfSkillsWithName extends GolfSkillsRecord { participantName: string; } export interface GolfSkillsInput { participantId: string; sportsSeasonId: string; sgTotal?: number | null; datagolfRank?: number | null; mastersOdds?: number | null; usOpenOdds?: number | null; openChampionshipOdds?: number | null; pgaChampionshipOdds?: number | null; } /** * Get all golf skill records for a sports season, joined with participant names. * Returns one record per participant (seasonParticipants with no record are excluded). */ export async function getGolfSkillsForSeason( sportsSeasonId: string ): Promise { const db = database(); const rows = await db .select({ id: participantGolfSkills.id, participantId: participantGolfSkills.participantId, sportsSeasonId: participantGolfSkills.sportsSeasonId, sgTotal: participantGolfSkills.sgTotal, datagolfRank: participantGolfSkills.datagolfRank, mastersOdds: participantGolfSkills.mastersOdds, usOpenOdds: participantGolfSkills.usOpenOdds, openChampionshipOdds: participantGolfSkills.openChampionshipOdds, pgaChampionshipOdds: participantGolfSkills.pgaChampionshipOdds, updatedAt: participantGolfSkills.updatedAt, participantName: seasonParticipants.name, }) .from(participantGolfSkills) .innerJoin(seasonParticipants, eq(participantGolfSkills.participantId, seasonParticipants.id)) .where(eq(participantGolfSkills.sportsSeasonId, sportsSeasonId)) .orderBy(seasonParticipants.name); return rows.map((r) => ({ ...r, sgTotal: r.sgTotal !== null ? Number(r.sgTotal) : null, })); } /** * Returns a Map from participantId → GolfSkillsRecord for use in the simulator. * Participants with no record are absent from the map (simulator falls back to SG = 0). */ export async function getGolfSkillsMap( sportsSeasonId: string ): Promise> { const db = database(); const rows = await db .select() .from(participantGolfSkills) .where(eq(participantGolfSkills.sportsSeasonId, sportsSeasonId)); return new Map(rows.map((r) => [ r.participantId, { ...r, sgTotal: r.sgTotal !== null ? Number(r.sgTotal) : null, }, ])); } /** * Upsert golf skill ratings for a batch of seasonParticipants. * Uses INSERT … ON CONFLICT DO UPDATE so all columns are overwritten atomically. */ export async function batchUpsertGolfSkills( inputs: GolfSkillsInput[] ): Promise { if (inputs.length === 0) return; const db = database(); const now = new Date(); await db .insert(participantGolfSkills) .values( inputs.map(({ participantId, sportsSeasonId, sgTotal, datagolfRank, mastersOdds, usOpenOdds, openChampionshipOdds, pgaChampionshipOdds, }) => ({ participantId, sportsSeasonId, // decimal columns are stored/passed as strings in Drizzle sgTotal: sgTotal !== null && sgTotal !== undefined ? String(sgTotal) : null, datagolfRank: datagolfRank ?? null, mastersOdds: mastersOdds ?? null, usOpenOdds: usOpenOdds ?? null, openChampionshipOdds: openChampionshipOdds ?? null, pgaChampionshipOdds: pgaChampionshipOdds ?? null, updatedAt: now, })) ) .onConflictDoUpdate({ target: [participantGolfSkills.participantId, participantGolfSkills.sportsSeasonId], set: { sgTotal: sql`excluded.sg_total`, datagolfRank: sql`excluded.datagolf_rank`, mastersOdds: sql`excluded.masters_odds`, usOpenOdds: sql`excluded.us_open_odds`, openChampionshipOdds: sql`excluded.open_championship_odds`, pgaChampionshipOdds: sql`excluded.pga_championship_odds`, updatedAt: sql`excluded.updated_at`, }, }); }