import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { eq, and, inArray, max, sql } from "drizzle-orm"; export interface UpsertSeasonResultData { participantId: string; sportsSeasonId: string; currentPoints?: number; currentPosition?: number; } export interface UpdateSeasonResultData { currentPoints?: number; currentPosition?: number; } /** * Upsert a single participant's season result atomically using ON CONFLICT DO UPDATE. */ export async function upsertParticipantSeasonResult( data: UpsertSeasonResultData, providedDb?: ReturnType ) { const db = providedDb || database(); const now = new Date(); const [row] = await db .insert(schema.participantSeasonResults) .values({ participantId: data.participantId, sportsSeasonId: data.sportsSeasonId, currentPoints: (data.currentPoints ?? 0).toString(), currentPosition: data.currentPosition ?? null, updatedAt: now, }) .onConflictDoUpdate({ target: [ schema.participantSeasonResults.participantId, schema.participantSeasonResults.sportsSeasonId, ], set: { currentPoints: sql`excluded.current_points`, currentPosition: sql`excluded.current_position`, updatedAt: sql`excluded.updated_at`, }, }) .returning(); return row; } /** * Bulk upsert season results for multiple participants in a single statement. */ export async function upsertSeasonResultsBulk( results: UpsertSeasonResultData[], providedDb?: ReturnType ) { const db = providedDb || database(); if (results.length === 0) return []; const now = new Date(); const values = results.map((data) => ({ participantId: data.participantId, sportsSeasonId: data.sportsSeasonId, currentPoints: (data.currentPoints ?? 0).toString(), currentPosition: data.currentPosition ?? null, updatedAt: now, })); return db .insert(schema.participantSeasonResults) .values(values) .onConflictDoUpdate({ target: [ schema.participantSeasonResults.participantId, schema.participantSeasonResults.sportsSeasonId, ], set: { currentPoints: sql`excluded.current_points`, currentPosition: sql`excluded.current_position`, updatedAt: sql`excluded.updated_at`, }, }) .returning(); } /** * Get a participant's season result */ export async function getParticipantSeasonResult( participantId: string, sportsSeasonId: string, providedDb?: ReturnType ) { const db = providedDb || database(); return await db.query.participantSeasonResults.findFirst({ where: and( eq(schema.participantSeasonResults.participantId, participantId), eq(schema.participantSeasonResults.sportsSeasonId, sportsSeasonId) ), with: { participant: { with: { sportsSeason: { with: { sport: true, }, }, }, }, }, }); } /** * Get all season results for a sports season * Returns participants ordered by current position (or points if position not set) */ export async function getSeasonResults( sportsSeasonId: string, providedDb?: ReturnType ) { const db = providedDb || database(); const results = await db.query.participantSeasonResults.findMany({ where: eq(schema.participantSeasonResults.sportsSeasonId, sportsSeasonId), with: { participant: { with: { sportsSeason: { with: { sport: true, }, }, }, }, }, }); // Sort by position (nulls last), then by points descending return results.toSorted((a, b) => { if (a.currentPosition !== null && b.currentPosition !== null) { return a.currentPosition - b.currentPosition; } if (a.currentPosition !== null) return -1; if (b.currentPosition !== null) return 1; // Both null positions, sort by points const aPoints = parseFloat(a.currentPoints || "0"); const bPoints = parseFloat(b.currentPoints || "0"); return bPoints - aPoints; }); } /** * Get season results for specific participants */ export async function getSeasonResultsForParticipants( sportsSeasonId: string, participantIds: string[], providedDb?: ReturnType ) { const db = providedDb || database(); if (participantIds.length === 0) return []; return await db.query.participantSeasonResults.findMany({ where: and( eq(schema.participantSeasonResults.sportsSeasonId, sportsSeasonId), inArray(schema.participantSeasonResults.participantId, participantIds) ), with: { participant: { with: { sportsSeason: { with: { sport: true, }, }, }, }, }, }); } /** * Update a participant's season result */ export async function updateParticipantSeasonResult( participantId: string, sportsSeasonId: string, data: UpdateSeasonResultData, providedDb?: ReturnType ) { const db = providedDb || database(); const [updated] = await db .update(schema.participantSeasonResults) .set({ currentPoints: data.currentPoints?.toString(), currentPosition: data.currentPosition, updatedAt: new Date(), }) .where( and( eq(schema.participantSeasonResults.participantId, participantId), eq(schema.participantSeasonResults.sportsSeasonId, sportsSeasonId) ) ) .returning(); return updated; } /** * Delete all season results for a sports season */ export async function deleteSeasonResults( sportsSeasonId: string, providedDb?: ReturnType ) { const db = providedDb || database(); await db .delete(schema.participantSeasonResults) .where(eq(schema.participantSeasonResults.sportsSeasonId, sportsSeasonId)); } /** * Check if a participant has a season result */ export async function hasParticipantSeasonResult( participantId: string, sportsSeasonId: string, providedDb?: ReturnType ): Promise { const db = providedDb || database(); const result = await db.query.participantSeasonResults.findFirst({ where: and( eq(schema.participantSeasonResults.participantId, participantId), eq(schema.participantSeasonResults.sportsSeasonId, sportsSeasonId) ), }); return !!result; } /** * Returns the most recent updatedAt timestamp across all season results for a sports season. * Used to display "Last synced" time for season_standings sports (F1, IndyCar). */ export async function getLastSeasonResultsSyncedAt( sportsSeasonId: string, providedDb?: ReturnType ): Promise { const db = providedDb || database(); const result = await db .select({ lastUpdated: max(schema.participantSeasonResults.updatedAt) }) .from(schema.participantSeasonResults) .where(eq(schema.participantSeasonResults.sportsSeasonId, sportsSeasonId)); return result[0]?.lastUpdated ?? null; }