import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { eq, and, inArray } from "drizzle-orm"; import { getScoringRules, calculateFantasyPoints, calculateAveragedPoints } from "./scoring-rules"; /** * Core scoring calculation engine * This file contains the logic for calculating fantasy points based on participant results */ export type ScoringPattern = | "single_elimination_playoff" | "page_playoff" | "season_standings" | "qualifying_points"; /** * Process a playoff event completion and assign final placements * Implementation will be added in Phase 2 */ export async function processPlayoffEvent( eventId: string, providedDb?: ReturnType ): Promise { const db = providedDb || database(); // TODO: Implement in Phase 2 // 1. Get event and determine round (QF, SF, Finals) // 2. Get matches and determine winners/losers // 3. Assign shared placements based on elimination round // 4. Update participantResults with final placements // 5. Trigger recalculation for affected leagues console.log(`[ScoringCalculator] processPlayoffEvent not yet implemented: ${eventId}`); } /** * Process a qualifying event completion and update QP totals * Implementation will be added in Phase 3 */ export async function processQualifyingEvent( eventId: string, providedDb?: ReturnType ): Promise { const db = providedDb || database(); // TODO: Implement in Phase 3 // 1. Get event results // 2. Award qualifying points based on placement // 3. Update participant_qualifying_totals // 4. Check if all majors complete // 5. Update QP standings displays console.log(`[ScoringCalculator] processQualifyingEvent not yet implemented: ${eventId}`); } /** * Finalize qualifying points and convert to fantasy placements * Called manually by admin when all majors are complete * Implementation will be added in Phase 3 */ export async function finalizeQualifyingPoints( sportsSeasonId: string, providedDb?: ReturnType ): Promise { const db = providedDb || database(); // TODO: Implement in Phase 3 // 1. Get all participants ranked by total QP // 2. Handle ties in QP (share placements) // 3. Assign final placements (1-8) based on QP ranking // 4. Update participantResults with final placements // 5. Mark sports season as finalized // 6. Trigger recalculation for all affected leagues console.log( `[ScoringCalculator] finalizeQualifyingPoints not yet implemented: ${sportsSeasonId}` ); } /** * Process season standings (F1) and assign final placements * Implementation will be added in Phase 3 */ export async function processSeasonStandings( sportsSeasonId: string, providedDb?: ReturnType ): Promise { const db = providedDb || database(); // TODO: Implement in Phase 3 // 1. Get final season standings from participant_season_results // 2. Assign final placements (1-8) based on standings // 3. Update participantResults with final placements // 4. Trigger recalculation for all affected leagues console.log( `[ScoringCalculator] processSeasonStandings not yet implemented: ${sportsSeasonId}` ); } /** * Calculate total fantasy points for a team in a specific season * Sums points from all drafted participants based on their final placements */ export async function calculateTeamScore( teamId: string, seasonId: string, providedDb?: ReturnType ): Promise<{ totalPoints: number; placementCounts: Record; participantsCompleted: number; participantsTotal: number; }> { const db = providedDb || database(); // Get season scoring rules const scoringRules = await getScoringRules(seasonId, db); if (!scoringRules) { throw new Error(`Season ${seasonId} not found`); } // Get all draft picks for this team const picks = await db.query.draftPicks.findMany({ where: and( eq(schema.draftPicks.teamId, teamId), eq(schema.draftPicks.seasonId, seasonId) ), with: { participant: { with: { results: true, }, }, }, }); let totalPoints = 0; const placementCounts: Record = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, }; let participantsCompleted = 0; for (const pick of picks) { const result = pick.participant.results[0]; // Should only be one result per participant if (result && result.finalPosition) { const points = calculateFantasyPoints(result.finalPosition, scoringRules); totalPoints += points; participantsCompleted++; // Track placement for tiebreakers if (result.finalPosition >= 1 && result.finalPosition <= 8) { placementCounts[result.finalPosition]++; } } } return { totalPoints, placementCounts, participantsCompleted, participantsTotal: picks.length, }; } /** * Recalculate standings for all teams in a season * Called after any scoring event completes or participant results change * Implementation will be completed in Phase 4 */ export async function recalculateStandings( seasonId: string, providedDb?: ReturnType ): Promise { const db = providedDb || database(); // TODO: Complete implementation in Phase 4 // For now, calculate basic scores without rankings const teams = await db.query.teams.findMany({ where: eq(schema.teams.seasonId, seasonId), }); for (const team of teams) { const teamScore = await calculateTeamScore(team.id, seasonId, db); // Upsert team standings const existing = await db.query.teamStandings.findFirst({ where: and( eq(schema.teamStandings.teamId, team.id), eq(schema.teamStandings.seasonId, seasonId) ), }); if (existing) { await db .update(schema.teamStandings) .set({ totalPoints: teamScore.totalPoints.toString(), firstPlaceCount: teamScore.placementCounts[1], secondPlaceCount: teamScore.placementCounts[2], thirdPlaceCount: teamScore.placementCounts[3], fourthPlaceCount: teamScore.placementCounts[4], fifthPlaceCount: teamScore.placementCounts[5], sixthPlaceCount: teamScore.placementCounts[6], seventhPlaceCount: teamScore.placementCounts[7], eighthPlaceCount: teamScore.placementCounts[8], participantsRemaining: teamScore.participantsTotal - teamScore.participantsCompleted, calculatedAt: new Date(), }) .where(eq(schema.teamStandings.id, existing.id)); } else { await db.insert(schema.teamStandings).values({ teamId: team.id, seasonId, totalPoints: teamScore.totalPoints.toString(), currentRank: 1, // Will be updated when we add ranking logic firstPlaceCount: teamScore.placementCounts[1], secondPlaceCount: teamScore.placementCounts[2], thirdPlaceCount: teamScore.placementCounts[3], fourthPlaceCount: teamScore.placementCounts[4], fifthPlaceCount: teamScore.placementCounts[5], sixthPlaceCount: teamScore.placementCounts[6], seventhPlaceCount: teamScore.placementCounts[7], eighthPlaceCount: teamScore.placementCounts[8], participantsRemaining: teamScore.participantsTotal - teamScore.participantsCompleted, }); } } console.log(`[ScoringCalculator] Recalculated standings for season ${seasonId}`); } /** * Recalculate standings for all leagues that include a specific sports season * Called after a sports season event completes */ export async function recalculateAffectedLeagues( sportsSeasonId: string, providedDb?: ReturnType ): Promise { const db = providedDb || database(); // Get all fantasy seasons that include this sports season const seasonSports = await db.query.seasonSports.findMany({ where: eq(schema.seasonSports.sportsSeasonId, sportsSeasonId), }); const seasonIds = seasonSports.map((ss) => ss.seasonId); // Recalculate each affected season for (const seasonId of seasonIds) { await recalculateStandings(seasonId, db); } console.log( `[ScoringCalculator] Recalculated ${seasonIds.length} affected season(s) for sports season ${sportsSeasonId}` ); }