import { eq, and, inArray } from "drizzle-orm"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; export type ParticipantResult = typeof schema.seasonParticipantResults.$inferSelect; export type ParticipantResultWithParticipant = ParticipantResult & { participant: { id: string; name: string } | null; }; export type NewParticipantResult = typeof schema.seasonParticipantResults.$inferInsert; export async function createParticipantResult( data: NewParticipantResult ): Promise { const db = database(); const [result] = await db .insert(schema.seasonParticipantResults) .values(data) .returning(); return result; } export async function createManyParticipantResults( data: NewParticipantResult[] ): Promise { const db = database(); return await db .insert(schema.seasonParticipantResults) .values(data) .returning(); } export async function findParticipantResultById( id: string ): Promise { const db = database(); return await db.query.seasonParticipantResults.findFirst({ where: eq(schema.seasonParticipantResults.id, id), with: { participant: true, sportsSeason: { with: { sport: true, }, }, }, }); } export async function findParticipantResultByParticipantId( participantId: string ): Promise { const db = database(); return await db.query.seasonParticipantResults.findFirst({ where: eq(schema.seasonParticipantResults.participantId, participantId), with: { participant: true, sportsSeason: { with: { sport: true, }, }, }, }); } export async function findParticipantResultsBySportsSeasonId( sportsSeasonId: string ): Promise { const db = database(); return await db.query.seasonParticipantResults.findMany({ where: eq(schema.seasonParticipantResults.sportsSeasonId, sportsSeasonId), orderBy: (results, { asc }) => [asc(results.finalPosition)], with: { participant: true, }, }); } /** * Tallies how many participants share each scoring placement, per sports season. * * Pure counterpart to getSharedPlacementCounts, for callers that already hold the * result rows. Split out so there is exactly one definition of what "tied" means * — the whole point of this module is that every screen counts ties identically. * * Positions <= 0 (no scoring placement) are excluded. */ export function countSharedPlacements( rows: Array<{ sportsSeasonId: string; finalPosition: number | null }> ): Map> { const counts = new Map>(); for (const row of rows) { if (row.finalPosition === null || row.finalPosition <= 0) continue; let bySeason = counts.get(row.sportsSeasonId); if (!bySeason) { bySeason = new Map(); counts.set(row.sportsSeasonId, bySeason); } bySeason.set(row.finalPosition, (bySeason.get(row.finalPosition) ?? 0) + 1); } return counts; } /** * How many participants share each scoring placement, per sports season. * * Returns sportsSeasonId → (finalPosition → count). Used to split a tied * placement's points across the tied participants (see calculatePickPoints). * * The count spans EVERY result in the sports season, not just drafted ones — a * golfer tied for 8th with an undrafted player still only earns half the 8th * place points, so narrowing this query to drafted participants would silently * over-award. * * Callers that look up a position with no entry should treat it as 1 (no tie). */ export async function getSharedPlacementCounts( sportsSeasonIds: string[], providedDb?: ReturnType ): Promise>> { if (sportsSeasonIds.length === 0) return new Map(); const db = providedDb || database(); const rows = await db.query.seasonParticipantResults.findMany({ where: inArray(schema.seasonParticipantResults.sportsSeasonId, sportsSeasonIds), columns: { sportsSeasonId: true, finalPosition: true }, }); return countSharedPlacements(rows); } /** * Convenience lookup over getSharedPlacementCounts' result. Missing entries mean * no other participant shares the placement, so the tie count is 1. */ export function lookupSharedPlacementCount( counts: Map>, sportsSeasonId: string, finalPosition: number ): number { return counts.get(sportsSeasonId)?.get(finalPosition) ?? 1; } export async function updateParticipantResult( id: string, data: Partial ): Promise { const db = database(); const [result] = await db .update(schema.seasonParticipantResults) .set({ ...data, updatedAt: new Date() }) .where(eq(schema.seasonParticipantResults.id, id)) .returning(); return result; } export async function deleteParticipantResult(id: string): Promise { const db = database(); await db.delete(schema.seasonParticipantResults).where(eq(schema.seasonParticipantResults.id, id)); } export async function deleteParticipantResultsBySportsSeasonId( sportsSeasonId: string, providedDb?: ReturnType ): Promise { const db = providedDb || database(); await db .delete(schema.seasonParticipantResults) .where(eq(schema.seasonParticipantResults.sportsSeasonId, sportsSeasonId)); } /** * Set result for a participant in a sports season * Points are calculated on-demand based on each fantasy league's scoring rules */ export async function setParticipantResult( participantId: string, sportsSeasonId: string, finalPosition: number, qualifyingPoints?: number, notes?: string ): Promise { const db = database(); // Check if result already exists const existing = await db.query.seasonParticipantResults.findFirst({ where: and( eq(schema.seasonParticipantResults.participantId, participantId), eq(schema.seasonParticipantResults.sportsSeasonId, sportsSeasonId) ), }); if (existing) { // Update existing result return await updateParticipantResult(existing.id, { finalPosition, qualifyingPoints: qualifyingPoints?.toString(), notes, }); } else { // Create new result return await createParticipantResult({ participantId, sportsSeasonId, finalPosition, qualifyingPoints: qualifyingPoints?.toString(), notes, }); } }