brackt/app/models/golf-skills.ts
Chris Parsons a18868e02b Fix oxlint errors: no-non-null-assertion and eqeqeq
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 21:42:02 -07:00

144 lines
4.5 KiB
TypeScript

/**
* 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, participants } 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 (participants with no record are excluded).
*/
export async function getGolfSkillsForSeason(
sportsSeasonId: string
): Promise<GolfSkillsWithName[]> {
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: participants.name,
})
.from(participantGolfSkills)
.innerJoin(participants, eq(participantGolfSkills.participantId, participants.id))
.where(eq(participantGolfSkills.sportsSeasonId, sportsSeasonId))
.orderBy(participants.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<Map<string, GolfSkillsRecord>> {
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 participants.
* Uses INSERT … ON CONFLICT DO UPDATE so all columns are overwritten atomically.
*/
export async function batchUpsertGolfSkills(
inputs: GolfSkillsInput[]
): Promise<void> {
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`,
},
});
}