/** * Model for EV (Expected Value) Snapshots * * Historical record of participant EVs and team projected points over time. * One snapshot per participant (or team) per sport season per day — upsert on conflict. * Snapshots are taken directly from simulation output to avoid partial-capture issues. */ import { database } from "~/database/context"; import { participantEvSnapshots, teamEvSnapshots } from "~/database/schema"; import { eq, and, asc } from "drizzle-orm"; // ─── Participant EV Snapshots ───────────────────────────────────────────────── export interface ParticipantEvSnapshotInput { participantId: string; sportsSeasonId: string; snapshotDate: string; // YYYY-MM-DD probFirst: number; probSecond: number; probThird: number; probFourth: number; probFifth: number; probSixth: number; probSeventh: number; probEighth: number; calculatedEV: number; source: string; } export interface ParticipantEvHistoryPoint { snapshotDate: string; calculatedEV: number; probFirst: number; } /** * Upsert a single participant EV snapshot. * If a snapshot already exists for this (participantId, sportsSeasonId, snapshotDate), * it will be overwritten with the new values. */ export async function upsertParticipantEvSnapshot( input: ParticipantEvSnapshotInput ): Promise { const db = database(); const now = new Date(); await db .insert(participantEvSnapshots) .values({ participantId: input.participantId, sportsSeasonId: input.sportsSeasonId, snapshotDate: input.snapshotDate, probFirst: input.probFirst.toString(), probSecond: input.probSecond.toString(), probThird: input.probThird.toString(), probFourth: input.probFourth.toString(), probFifth: input.probFifth.toString(), probSixth: input.probSixth.toString(), probSeventh: input.probSeventh.toString(), probEighth: input.probEighth.toString(), calculatedEV: input.calculatedEV.toString(), source: input.source, createdAt: now, updatedAt: now, }) .onConflictDoUpdate({ target: [ participantEvSnapshots.participantId, participantEvSnapshots.sportsSeasonId, participantEvSnapshots.snapshotDate, ], set: { probFirst: input.probFirst.toString(), probSecond: input.probSecond.toString(), probThird: input.probThird.toString(), probFourth: input.probFourth.toString(), probFifth: input.probFifth.toString(), probSixth: input.probSixth.toString(), probSeventh: input.probSeventh.toString(), probEighth: input.probEighth.toString(), calculatedEV: input.calculatedEV.toString(), source: input.source, updatedAt: now, }, }); } /** * Batch upsert participant EV snapshots within a single transaction. * All snapshots succeed or all fail together. */ export async function batchUpsertParticipantEvSnapshots( inputs: ParticipantEvSnapshotInput[] ): Promise { if (inputs.length === 0) return; const db = database(); const now = new Date(); await db.transaction(async (tx) => { for (const input of inputs) { await tx .insert(participantEvSnapshots) .values({ participantId: input.participantId, sportsSeasonId: input.sportsSeasonId, snapshotDate: input.snapshotDate, probFirst: input.probFirst.toString(), probSecond: input.probSecond.toString(), probThird: input.probThird.toString(), probFourth: input.probFourth.toString(), probFifth: input.probFifth.toString(), probSixth: input.probSixth.toString(), probSeventh: input.probSeventh.toString(), probEighth: input.probEighth.toString(), calculatedEV: input.calculatedEV.toString(), source: input.source, createdAt: now, updatedAt: now, }) .onConflictDoUpdate({ target: [ participantEvSnapshots.participantId, participantEvSnapshots.sportsSeasonId, participantEvSnapshots.snapshotDate, ], set: { probFirst: input.probFirst.toString(), probSecond: input.probSecond.toString(), probThird: input.probThird.toString(), probFourth: input.probFourth.toString(), probFifth: input.probFifth.toString(), probSixth: input.probSixth.toString(), probSeventh: input.probSeventh.toString(), probEighth: input.probEighth.toString(), calculatedEV: input.calculatedEV.toString(), source: input.source, updatedAt: now, }, }); } }); } /** * Get EV history for a single participant in a sport season, ordered oldest first. * Used for EV trend charts. */ export async function getParticipantEvHistory( participantId: string, sportsSeasonId: string ): Promise { const db = database(); const rows = await db .select({ snapshotDate: participantEvSnapshots.snapshotDate, calculatedEV: participantEvSnapshots.calculatedEV, probFirst: participantEvSnapshots.probFirst, }) .from(participantEvSnapshots) .where( and( eq(participantEvSnapshots.participantId, participantId), eq(participantEvSnapshots.sportsSeasonId, sportsSeasonId) ) ) .orderBy(asc(participantEvSnapshots.snapshotDate)); return rows.map((r) => ({ snapshotDate: r.snapshotDate, calculatedEV: parseFloat(r.calculatedEV), probFirst: parseFloat(r.probFirst), })); } // ─── Team EV Snapshots ──────────────────────────────────────────────────────── export interface TeamEvSnapshotInput { teamId: string; seasonId: string; snapshotDate: string; // YYYY-MM-DD projectedPoints: number; actualPoints: number; } export interface TeamEvHistoryPoint { snapshotDate: string; projectedPoints: number; actualPoints: number; } /** * Upsert a team EV snapshot for a given day. */ export async function upsertTeamEvSnapshot( input: TeamEvSnapshotInput ): Promise { const db = database(); const now = new Date(); await db .insert(teamEvSnapshots) .values({ teamId: input.teamId, seasonId: input.seasonId, snapshotDate: input.snapshotDate, projectedPoints: input.projectedPoints.toString(), actualPoints: input.actualPoints.toString(), createdAt: now, }) .onConflictDoUpdate({ target: [ teamEvSnapshots.teamId, teamEvSnapshots.seasonId, teamEvSnapshots.snapshotDate, ], set: { projectedPoints: input.projectedPoints.toString(), actualPoints: input.actualPoints.toString(), }, }); } /** * Get projected points history for a team in a fantasy season, ordered oldest first. * Used for team trend charts. */ export async function getTeamEvHistory( teamId: string, seasonId: string ): Promise { const db = database(); const rows = await db .select({ snapshotDate: teamEvSnapshots.snapshotDate, projectedPoints: teamEvSnapshots.projectedPoints, actualPoints: teamEvSnapshots.actualPoints, }) .from(teamEvSnapshots) .where( and( eq(teamEvSnapshots.teamId, teamId), eq(teamEvSnapshots.seasonId, seasonId) ) ) .orderBy(asc(teamEvSnapshots.snapshotDate)); return rows.map((r) => ({ snapshotDate: r.snapshotDate, projectedPoints: parseFloat(r.projectedPoints), actualPoints: parseFloat(r.actualPoints), })); }