Centralize futures odds onto the simulator page and fix bulk import #117
3 changed files with 33 additions and 4 deletions
|
|
@ -69,6 +69,15 @@ describe("batchUpsertParticipantSimulatorInputs", () => {
|
||||||
expect(sourceEloSql).toContain("coalesce");
|
expect(sourceEloSql).toContain("coalesce");
|
||||||
const ratingSql = sqlToText(simulatorSet.rating).toLowerCase();
|
const ratingSql = sqlToText(simulatorSet.rating).toLowerCase();
|
||||||
expect(ratingSql).toContain("coalesce");
|
expect(ratingSql).toContain("coalesce");
|
||||||
|
|
||||||
|
// Metadata must drop the per-column method flag whenever that column gets a
|
||||||
|
// fresh direct value, so a stale "generated" flag can't hide a newly entered
|
||||||
|
// Elo/rating. Blindly preserving metadata (e.g. COALESCE) would be the bug.
|
||||||
|
const metadataSql = sqlToText(simulatorSet.metadata).toLowerCase();
|
||||||
|
expect(metadataSql).toContain("sourceelomethod");
|
||||||
|
expect(metadataSql).toContain("ratingmethod");
|
||||||
|
expect(metadataSql).toContain("excluded.source_elo");
|
||||||
|
expect(metadataSql).toContain("excluded.rating");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("is a no-op when given no inputs", async () => {
|
it("is a no-op when given no inputs", async () => {
|
||||||
|
|
|
||||||
|
|
@ -353,7 +353,20 @@ export async function batchUpsertParticipantSimulatorInputs(
|
||||||
projectedTablePoints: sql`COALESCE(excluded.projected_table_points, ${schema.seasonParticipantSimulatorInputs.projectedTablePoints})`,
|
projectedTablePoints: sql`COALESCE(excluded.projected_table_points, ${schema.seasonParticipantSimulatorInputs.projectedTablePoints})`,
|
||||||
seed: sql`COALESCE(excluded.seed, ${schema.seasonParticipantSimulatorInputs.seed})`,
|
seed: sql`COALESCE(excluded.seed, ${schema.seasonParticipantSimulatorInputs.seed})`,
|
||||||
region: sql`COALESCE(excluded.region, ${schema.seasonParticipantSimulatorInputs.region})`,
|
region: sql`COALESCE(excluded.region, ${schema.seasonParticipantSimulatorInputs.region})`,
|
||||||
metadata: sql`COALESCE(excluded.metadata, ${schema.seasonParticipantSimulatorInputs.metadata})`,
|
// Metadata carries the method flags (sourceEloMethod/ratingMethod) that
|
||||||
|
// tell readers whether the stored Elo/rating is generated vs. a trusted
|
||||||
|
// direct value. When a caller supplies explicit metadata, use it as-is
|
||||||
|
// (prepareSimulatorInputsForRun and the projection importer set the
|
||||||
|
// correct flags). Otherwise preserve existing metadata, but drop the
|
||||||
|
// method flag for any column receiving a fresh direct value — otherwise a
|
||||||
|
// stale "generated" flag would cause that newly-entered Elo/rating to be
|
||||||
|
// filtered out as derived (see getParticipantSimulatorInputs).
|
||||||
|
metadata: sql`CASE
|
||||||
|
WHEN excluded.metadata IS NOT NULL THEN excluded.metadata
|
||||||
|
ELSE COALESCE(${schema.seasonParticipantSimulatorInputs.metadata}, '{}'::jsonb)
|
||||||
|
- (CASE WHEN excluded.source_elo IS NOT NULL THEN 'sourceEloMethod' ELSE '' END)
|
||||||
|
- (CASE WHEN excluded.rating IS NOT NULL THEN 'ratingMethod' ELSE '' END)
|
||||||
|
END`,
|
||||||
updatedAt: sql`excluded.updated_at`,
|
updatedAt: sql`excluded.updated_at`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -280,14 +280,21 @@ export async function action({ request, params }: Route.ActionArgs): Promise<Act
|
||||||
const saved = `Saved ${parsed.inputs.length} simulator input row(s).${suffix}`;
|
const saved = `Saved ${parsed.inputs.length} simulator input row(s).${suffix}`;
|
||||||
|
|
||||||
// Auto-run the simulation so saved inputs immediately drive the standings.
|
// Auto-run the simulation so saved inputs immediately drive the standings.
|
||||||
// The save itself already succeeded, so a not-ready run (e.g. participants
|
// The save itself already succeeded, so a run that never started (e.g.
|
||||||
// missing required inputs) is reported as success with the readiness gap —
|
// participants missing required inputs) is reported as success with the
|
||||||
// never as a failed save.
|
// readiness gap — never as a failed save.
|
||||||
try {
|
try {
|
||||||
await runSportsSeasonSimulation(sportsSeasonId);
|
await runSportsSeasonSimulation(sportsSeasonId);
|
||||||
return redirect(`/admin/sports-seasons/${sportsSeasonId}/expected-values`);
|
return redirect(`/admin/sports-seasons/${sportsSeasonId}/expected-values`);
|
||||||
} catch (runError) {
|
} catch (runError) {
|
||||||
const reason = runError instanceof Error ? runError.message : "could not run.";
|
const reason = runError instanceof Error ? runError.message : "could not run.";
|
||||||
|
// Distinguish "saved but never ran" (readiness/already-running) from
|
||||||
|
// "started running and failed mid-run": the runner only flips the season
|
||||||
|
// to status 'failed' once the simulation itself throws.
|
||||||
|
const season = await findSportsSeasonById(sportsSeasonId);
|
||||||
|
if (season?.simulationStatus === "failed") {
|
||||||
|
return { success: false, message: `${saved} The simulation failed: ${reason}` };
|
||||||
|
}
|
||||||
return { success: true, message: `${saved} Simulation not run yet: ${reason}` };
|
return { success: true, message: `${saved} Simulation not run yet: ${reason}` };
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue