Include participant EV/futures data in data sync export/import

The export now includes the full participantExpectedValues records
(placement probabilities, source, sourceOdds) alongside participants.
On import, EV records are upserted in merge mode and created fresh in
replace mode (cascade handles cleanup). Backward-compatible with older
exports that lack the participantExpectedValues field.

https://claude.ai/code/session_01TVzuuDR7jPtmihaXTrsX7B
This commit is contained in:
Claude 2026-02-21 03:45:00 +00:00
parent 401efcd833
commit dd5072a2c2
No known key found for this signature in database
2 changed files with 122 additions and 1 deletions

View file

@ -221,7 +221,7 @@ export default function DataSync({ loaderData, actionData }: Route.ComponentProp
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-muted-foreground">
This will export all sports, sports seasons, participants, and season templates.
This will export all sports, sports seasons, participants, participant EV/futures data, and season templates.
</p>
<Button
@ -313,6 +313,7 @@ export default function DataSync({ loaderData, actionData }: Route.ComponentProp
<li>All sports</li>
<li>All sports seasons</li>
<li>All participants</li>
<li>All participant EV/futures data</li>
<li>All season templates</li>
</ul>
</AlertDialogDescription>

View file

@ -29,6 +29,23 @@ interface ExportData {
externalId: string | null;
expectedValue: number | string;
}>;
participantExpectedValues?: Array<{
sportSlug: string;
seasonName: string;
seasonYear: number;
participantName: string;
probFirst: string;
probSecond: string;
probThird: string;
probFourth: string;
probFifth: string;
probSixth: string;
probSeventh: string;
probEighth: string;
expectedValue: string;
source: string | null;
sourceOdds: number | null;
}>;
seasonTemplates: Array<{
name: string;
year: number;
@ -73,6 +90,21 @@ export async function exportSportsDataToJSON(): Promise<ExportData> {
orderBy: (participants, { asc }) => [asc(participants.name)],
});
// Export participant expected values with relations
const participantEVs = await db.query.participantExpectedValues.findMany({
with: {
participant: {
with: {
sportsSeason: {
with: {
sport: true,
},
},
},
},
},
});
// Export season templates with sports
const seasonTemplates = await db.query.seasonTemplates.findMany({
with: {
@ -117,6 +149,23 @@ export async function exportSportsDataToJSON(): Promise<ExportData> {
externalId: participant.externalId,
expectedValue: participant.expectedValue,
})),
participantExpectedValues: participantEVs.map((ev) => ({
sportSlug: ev.participant.sportsSeason.sport.slug,
seasonName: ev.participant.sportsSeason.name,
seasonYear: ev.participant.sportsSeason.year,
participantName: ev.participant.name,
probFirst: ev.probFirst,
probSecond: ev.probSecond,
probThird: ev.probThird,
probFourth: ev.probFourth,
probFifth: ev.probFifth,
probSixth: ev.probSixth,
probSeventh: ev.probSeventh,
probEighth: ev.probEighth,
expectedValue: ev.expectedValue,
source: ev.source,
sourceOdds: ev.sourceOdds,
})),
seasonTemplates: seasonTemplates.map((template) => ({
name: template.name,
year: template.year,
@ -272,6 +321,77 @@ export async function importSportsDataFromJSON(
}
}
// Import participant expected values (optional field for backward compatibility)
if (importData.participantExpectedValues) {
for (const ev of importData.participantExpectedValues) {
const seasonKey = `${ev.sportSlug}:${ev.seasonName}:${ev.seasonYear}`;
const sportsSeasonId = sportsSeasonIdMap.get(seasonKey);
if (!sportsSeasonId) {
skipped++;
continue;
}
const participant = await db.query.participants.findFirst({
where: and(
eq(schema.participants.sportsSeasonId, sportsSeasonId),
eq(schema.participants.name, ev.participantName)
),
});
if (!participant) {
skipped++;
continue;
}
const existingEV = await db.query.participantExpectedValues.findFirst({
where: and(
eq(schema.participantExpectedValues.participantId, participant.id),
eq(schema.participantExpectedValues.sportsSeasonId, sportsSeasonId)
),
});
const evValues = {
probFirst: ev.probFirst,
probSecond: ev.probSecond,
probThird: ev.probThird,
probFourth: ev.probFourth,
probFifth: ev.probFifth,
probSixth: ev.probSixth,
probSeventh: ev.probSeventh,
probEighth: ev.probEighth,
expectedValue: ev.expectedValue,
source: ev.source as "manual" | "futures_odds" | "elo_simulation" | "performance_model" | null,
sourceOdds: ev.sourceOdds,
};
if (existingEV) {
if (mode === "merge") {
await db
.update(schema.participantExpectedValues)
.set({ ...evValues, updatedAt: new Date() })
.where(eq(schema.participantExpectedValues.id, existingEV.id));
await db
.update(schema.participants)
.set({ expectedValue: ev.expectedValue })
.where(eq(schema.participants.id, participant.id));
updated++;
}
} else {
await db.insert(schema.participantExpectedValues).values({
participantId: participant.id,
sportsSeasonId,
...evValues,
});
await db
.update(schema.participants)
.set({ expectedValue: ev.expectedValue })
.where(eq(schema.participants.id, participant.id));
created++;
}
}
}
// Import season templates
for (const template of importData.seasonTemplates) {
const existing = await db.query.seasonTemplates.findFirst({