From 3f3401f0d852ae22c9798cac4c215717ad8d78fd Mon Sep 17 00:00:00 2001 From: Chris Parsons <438676+chrisparsons83@users.noreply.github.com> Date: Fri, 20 Mar 2026 14:39:05 -0700 Subject: [PATCH] Recalculate league standings after EV simulation updates (#186) * Force standings recalculation after EV simulation After persisting updated EVs to participantExpectedValues, call recalculateAffectedLeagues so that teamStandings.projectedPoints (and all related standing fields) immediately reflect the new EV rather than remaining stale until the next scoring event. https://claude.ai/code/session_01S9Pk4wTSphANMhPWaURPTn * Use recalculateStandings directly after EV simulation Replaces recalculateAffectedLeagues (which carries Discord notification and daily snapshot side effects) with a direct call to recalculateStandings per affected fantasy season. This updates the cached projectedPoints in teamStandings so the standings page stays in sync with the team page after a simulation, without triggering unrelated side effects. https://claude.ai/code/session_01S9Pk4wTSphANMhPWaURPTn * Polish simulate route after code review - Remove redundant db variable; use database() inline consistent with all other model calls in this file - Parallelize standings recalculation with Promise.all - Update file header comment to include the standings refresh step https://claude.ai/code/session_01S9Pk4wTSphANMhPWaURPTn --------- Co-authored-by: Claude --- app/routes/admin.sports-seasons.$id.simulate.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/app/routes/admin.sports-seasons.$id.simulate.tsx b/app/routes/admin.sports-seasons.$id.simulate.tsx index 0ccc474..5fc9bc2 100644 --- a/app/routes/admin.sports-seasons.$id.simulate.tsx +++ b/app/routes/admin.sports-seasons.$id.simulate.tsx @@ -5,8 +5,9 @@ * 1. Guards against concurrent runs via simulationStatus * 2. Runs the appropriate simulator based on the sport's simulatorType * 3. Persists updated EVs to participantExpectedValues (transactional) - * 4. Takes an EV snapshot from simulation output (upsert for today's date) - * 5. Redirects back to the sports season admin page + * 4. Refreshes cached projectedPoints in teamStandings for all affected fantasy seasons + * 5. Takes an EV snapshot from simulation output (upsert for today's date) + * 6. Redirects back to the sports season admin page */ import { redirect } from "react-router"; @@ -17,6 +18,10 @@ import { batchUpsertParticipantEvSnapshots } from "~/models/ev-snapshot"; import { findParticipantsBySportsSeasonId } from "~/models/participant"; import { getSimulator, type SimulatorType } from "~/services/simulations/registry"; import { calculateEV, type ScoringRules } from "~/services/ev-calculator"; +import { recalculateStandings } from "~/models/scoring-calculator"; +import { database } from "~/database/context"; +import * as schema from "~/database/schema"; +import { eq } from "drizzle-orm"; // Default scoring rules — matches the season table defaults. // Per-league EV is recalculated from probabilities using league-specific rules in standings. @@ -99,6 +104,12 @@ export async function action({ params }: Route.ActionArgs) { // Persist updated EVs (transactional) await batchUpsertParticipantEVs(evInputs); + // Refresh cached projectedPoints in teamStandings for all affected fantasy seasons + const seasonSports = await database().query.seasonSports.findMany({ + where: eq(schema.seasonSports.sportsSeasonId, sportsSeasonId), + }); + await Promise.all(seasonSports.map(({ seasonId }) => recalculateStandings(seasonId))); + // Take EV snapshot from simulation output (not re-read from DB) const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD await batchUpsertParticipantEvSnapshots(