From 3210ab265ffcde12a344b3a337f354e719a2288e Mon Sep 17 00:00:00 2001 From: Chris Parsons <438676+chrisparsons83@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:26:40 -0800 Subject: [PATCH] Change participant expectedValue from integer to decimal (#1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: sync odds-based EV to participants table for draft room ranking When admin enters futures odds or manual probabilities, the calculated EV is now synced from participantExpectedValues to participants.expectedValue. This closes the gap where EVs were calculated but never reflected in the draft room ranking. Also changes expectedValue from integer to decimal(10,2) for better precision, and displays "—" for participants with no odds data. https://claude.ai/code/session_01JWG2zg2EMzCn6RgEufPC1T * fix: correct expectedValue type to string in participants route Missed instance of `expectedValue: 0` (number) that failed typecheck after schema change from integer to decimal. https://claude.ai/code/session_01JWG2zg2EMzCn6RgEufPC1T --------- Co-authored-by: Claude --- .../draft/AvailableParticipantsSection.tsx | 2 +- app/models/draft-utils.ts | 8 +++-- app/models/participant-expected-value.ts | 33 ++++++++++++++++--- .../admin.sports-seasons.$id.participants.tsx | 4 +-- .../leagues/$leagueId.draft.$seasonId.tsx | 8 ++++- app/utils/sports-data-sync.server.ts | 4 +-- database/schema.ts | 2 +- .../0029_change_participant_ev_to_decimal.sql | 1 + drizzle/meta/_journal.json | 7 ++++ 9 files changed, 54 insertions(+), 15 deletions(-) create mode 100644 drizzle/0029_change_participant_ev_to_decimal.sql diff --git a/app/components/draft/AvailableParticipantsSection.tsx b/app/components/draft/AvailableParticipantsSection.tsx index 05addc2..53d5824 100644 --- a/app/components/draft/AvailableParticipantsSection.tsx +++ b/app/components/draft/AvailableParticipantsSection.tsx @@ -193,7 +193,7 @@ export function AvailableParticipantsSection({ {participant.sport.name} - {participant.expectedValue} + {participant.expectedValue ? participant.expectedValue.toFixed(1) : "—"} {hasTeam && ( diff --git a/app/models/draft-utils.ts b/app/models/draft-utils.ts index 91c4e27..9a202f5 100644 --- a/app/models/draft-utils.ts +++ b/app/models/draft-utils.ts @@ -294,10 +294,12 @@ export async function getTopAvailableParticipant( allParticipants.push(...seasonParticipants); } - // Sort by EV desc, then name + // Sort by EV desc, then name (expectedValue is a decimal string from DB) allParticipants.sort((a, b) => { - if (b.expectedValue !== a.expectedValue) { - return b.expectedValue - a.expectedValue; + const evA = parseFloat(String(a.expectedValue)) || 0; + const evB = parseFloat(String(b.expectedValue)) || 0; + if (evB !== evA) { + return evB - evA; } return a.name.localeCompare(b.name); }); diff --git a/app/models/participant-expected-value.ts b/app/models/participant-expected-value.ts index bb455c2..a4f3296 100644 --- a/app/models/participant-expected-value.ts +++ b/app/models/participant-expected-value.ts @@ -6,7 +6,7 @@ */ import { database } from "~/database/context"; -import { participantExpectedValues } from "~/database/schema"; +import { participantExpectedValues, participants } from "~/database/schema"; import { eq, and } from "drizzle-orm"; import type { ProbabilityDistribution, ScoringRules } from "~/services/ev-calculator"; import { calculateEV, validateProbabilities, normalizeProbabilities } from "~/services/ev-calculator"; @@ -95,6 +95,8 @@ export async function upsertParticipantEV( const now = new Date(); + let result: ParticipantEV; + if (existing.length > 0) { // Update existing const updated = await db @@ -117,7 +119,7 @@ export async function upsertParticipantEV( .where(eq(participantExpectedValues.id, existing[0].id)) .returning(); - return updated[0]; + result = updated[0]; } else { // Create new const created = await db @@ -141,8 +143,16 @@ export async function upsertParticipantEV( }) .returning(); - return created[0]; + result = created[0]; } + + // Sync calculated EV to participants table for draft room ranking + await db + .update(participants) + .set({ expectedValue: expectedValue.toString(), updatedAt: now }) + .where(eq(participants.id, participantId)); + + return result; } /** @@ -211,6 +221,12 @@ export async function deleteParticipantEV( eq(participantExpectedValues.sportsSeasonId, sportsSeasonId) ) ); + + // Reset participant EV to 0 + await db + .update(participants) + .set({ expectedValue: "0", updatedAt: new Date() }) + .where(eq(participants.id, participantId)); } /** @@ -270,16 +286,23 @@ export async function recalculateEV( const newEV = calculateEV(probabilities, newScoringRules); const db = database(); + const now = new Date(); const updated = await db .update(participantExpectedValues) .set({ expectedValue: newEV.toString(), - calculatedAt: new Date(), - updatedAt: new Date(), + calculatedAt: now, + updatedAt: now, }) .where(eq(participantExpectedValues.id, existing.id)) .returning(); + // Sync recalculated EV to participants table + await db + .update(participants) + .set({ expectedValue: newEV.toString(), updatedAt: now }) + .where(eq(participants.id, participantId)); + return updated[0]; } diff --git a/app/routes/admin.sports-seasons.$id.participants.tsx b/app/routes/admin.sports-seasons.$id.participants.tsx index e5caeaa..0b3c665 100644 --- a/app/routes/admin.sports-seasons.$id.participants.tsx +++ b/app/routes/admin.sports-seasons.$id.participants.tsx @@ -79,7 +79,7 @@ export async function action({ request, params }: Route.ActionArgs) { name, shortName: null, externalId: null, - expectedValue: 0, + expectedValue: "0", }); } @@ -103,7 +103,7 @@ export async function action({ request, params }: Route.ActionArgs) { name: name.trim(), shortName: null, externalId: null, - expectedValue: 0, + expectedValue: "0", }); return { success: true }; diff --git a/app/routes/leagues/$leagueId.draft.$seasonId.tsx b/app/routes/leagues/$leagueId.draft.$seasonId.tsx index c4bd547..5eb0700 100644 --- a/app/routes/leagues/$leagueId.draft.$seasonId.tsx +++ b/app/routes/leagues/$leagueId.draft.$seasonId.tsx @@ -144,6 +144,12 @@ export async function loader(args: any) { desc(schema.participants.expectedValue), asc(schema.participants.name) ); + + // Parse decimal expectedValue (Drizzle returns strings for decimal columns) + availableParticipants = availableParticipants.map((p: any) => ({ + ...p, + expectedValue: parseFloat(p.expectedValue) || 0, + })); } // Load user's team queue if they have a team @@ -1054,7 +1060,7 @@ export default function DraftRoom() { {participant.sport.name} - {participant.expectedValue} + {participant.expectedValue ? participant.expectedValue.toFixed(1) : "—"}