import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { eq, and, max, sql } from "drizzle-orm"; export interface UpsertRegularSeasonStandingData { participantId: string; sportsSeasonId: string; wins: number; losses: number; otLosses?: number | null; ties?: number | null; winPct?: number | null; gamesPlayed: number; gamesBack?: number | null; conference?: string | null; division?: string | null; conferenceRank?: number | null; divisionRank?: number | null; leagueRank?: number | null; streak?: string | null; lastTen?: string | null; homeRecord?: string | null; awayRecord?: string | null; externalTeamId?: string | null; srs?: number | null; syncedAt?: Date | null; } /** * Bulk upsert regular season standings records. * Uses onConflictDoUpdate on the (participantId, sportsSeasonId) unique index. * Sets syncedAt = now() for auto-synced records; null means manually entered. */ export async function upsertRegularSeasonStandings( records: UpsertRegularSeasonStandingData[], providedDb?: ReturnType ) { const db = providedDb || database(); if (records.length === 0) return []; const now = new Date(); const values = records.map((r) => ({ participantId: r.participantId, sportsSeasonId: r.sportsSeasonId, wins: r.wins, losses: r.losses, otLosses: r.otLosses ?? null, ties: r.ties ?? null, winPct: r.winPct !== null && r.winPct !== undefined ? r.winPct.toString() : null, gamesPlayed: r.gamesPlayed, gamesBack: r.gamesBack !== null && r.gamesBack !== undefined ? r.gamesBack.toString() : null, conference: r.conference ?? null, division: r.division ?? null, conferenceRank: r.conferenceRank ?? null, divisionRank: r.divisionRank ?? null, leagueRank: r.leagueRank ?? null, streak: r.streak ?? null, lastTen: r.lastTen ?? null, homeRecord: r.homeRecord ?? null, awayRecord: r.awayRecord ?? null, externalTeamId: r.externalTeamId ?? null, srs: r.srs !== null && r.srs !== undefined ? r.srs.toString() : null, syncedAt: r.syncedAt !== undefined ? r.syncedAt : now, updatedAt: now, })); return await db.transaction(async (tx) => { return tx .insert(schema.regularSeasonStandings) .values(values) .onConflictDoUpdate({ target: [ schema.regularSeasonStandings.participantId, schema.regularSeasonStandings.sportsSeasonId, ], set: { wins: sql`excluded.wins`, losses: sql`excluded.losses`, otLosses: sql`excluded.ot_losses`, ties: sql`excluded.ties`, winPct: sql`excluded.win_pct`, gamesPlayed: sql`excluded.games_played`, gamesBack: sql`excluded.games_back`, conference: sql`excluded.conference`, division: sql`excluded.division`, conferenceRank: sql`excluded.conference_rank`, divisionRank: sql`excluded.division_rank`, leagueRank: sql`excluded.league_rank`, streak: sql`excluded.streak`, lastTen: sql`excluded.last_ten`, homeRecord: sql`excluded.home_record`, awayRecord: sql`excluded.away_record`, externalTeamId: sql`excluded.external_team_id`, srs: sql`excluded.srs`, syncedAt: sql`excluded.synced_at`, updatedAt: sql`excluded.updated_at`, }, }) .returning(); }); } /** * Upsert a single standing record as a manual admin override. * Sets syncedAt = null to indicate manual entry (won't conflict with auto-sync). */ export async function upsertManualStanding( participantId: string, sportsSeasonId: string, data: Omit, providedDb?: ReturnType ) { const results = await upsertRegularSeasonStandings( [{ ...data, participantId, sportsSeasonId, syncedAt: null }], providedDb ); return results[0]; } /** * Get all regular season standings for a sports season. * Ordered by conference, division, then divisionRank (or leagueRank as fallback). */ export async function getRegularSeasonStandings( sportsSeasonId: string, providedDb?: ReturnType ) { const db = providedDb || database(); const rows = await db.query.regularSeasonStandings.findMany({ where: eq(schema.regularSeasonStandings.sportsSeasonId, sportsSeasonId), with: { participant: true, }, }); // Sort: conference → division → divisionRank ?? leagueRank ?? leagueRank, nulls last return rows.toSorted((a, b) => { const confA = a.conference ?? "ZZZ"; const confB = b.conference ?? "ZZZ"; if (confA !== confB) return confA.localeCompare(confB); const divA = a.division ?? "ZZZ"; const divB = b.division ?? "ZZZ"; if (divA !== divB) return divA.localeCompare(divB); const rankA = a.divisionRank ?? a.leagueRank ?? 999; const rankB = b.divisionRank ?? b.leagueRank ?? 999; return rankA - rankB; }); } /** * Returns the most recent syncedAt timestamp across all records for a season. * Returns null if no auto-synced records exist. */ export async function getLastSyncedAt( sportsSeasonId: string, providedDb?: ReturnType ): Promise { const db = providedDb || database(); const result = await db .select({ lastSync: max(schema.regularSeasonStandings.syncedAt) }) .from(schema.regularSeasonStandings) .where(eq(schema.regularSeasonStandings.sportsSeasonId, sportsSeasonId)); return result[0]?.lastSync ?? null; } /** * Delete all regular season standings for a sports season. */ export async function deleteRegularSeasonStandings( sportsSeasonId: string, providedDb?: ReturnType ) { const db = providedDb || database(); await db .delete(schema.regularSeasonStandings) .where(eq(schema.regularSeasonStandings.sportsSeasonId, sportsSeasonId)); } /** * Get a single standing record for a participant in a season. */ export async function getParticipantStanding( participantId: string, sportsSeasonId: string, providedDb?: ReturnType ) { const db = providedDb || database(); return db.query.regularSeasonStandings.findFirst({ where: and( eq(schema.regularSeasonStandings.participantId, participantId), eq(schema.regularSeasonStandings.sportsSeasonId, sportsSeasonId) ), with: { participant: true }, }); }