87 lines
2.8 KiB
TypeScript
87 lines
2.8 KiB
TypeScript
|
|
/**
|
||
|
|
* Race-calendar state for season-standings sports (F1, IndyCar).
|
||
|
|
*
|
||
|
|
* Kept in its own leaf module rather than in `scoring-event.ts` so that
|
||
|
|
* `simulator.ts` can read it: `scoring-event.ts` pulls in `scoring-calculator`,
|
||
|
|
* which reaches `participant-expected-value` and back into `simulator`. This
|
||
|
|
* file imports nothing but the database.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { eq } from "drizzle-orm";
|
||
|
|
import { database } from "~/database/context";
|
||
|
|
import * as schema from "~/database/schema";
|
||
|
|
|
||
|
|
export interface SeasonRaceCounts {
|
||
|
|
completed: number;
|
||
|
|
remaining: number;
|
||
|
|
total: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Has this race already been run?
|
||
|
|
*
|
||
|
|
* `is_complete` wins when an admin has set it, but a racing calendar is stored
|
||
|
|
* as "Non-Scoring" rows that nobody ever marks complete, so the date is the real
|
||
|
|
* signal. Mirrors the Upcoming / Results Pending badge on the admin events page.
|
||
|
|
* A race happening today is still upcoming, and a row with no date at all counts
|
||
|
|
* as upcoming.
|
||
|
|
*
|
||
|
|
* @param today `now` as a `YYYY-MM-DD` string, to compare against the date-only
|
||
|
|
* `event_date` column.
|
||
|
|
*/
|
||
|
|
export function hasRaceRun(
|
||
|
|
event: {
|
||
|
|
isComplete: boolean;
|
||
|
|
eventDate: string | null;
|
||
|
|
eventStartsAt: Date | string | null;
|
||
|
|
},
|
||
|
|
now: Date,
|
||
|
|
today: string
|
||
|
|
): boolean {
|
||
|
|
if (event.isComplete) return true;
|
||
|
|
if (event.eventStartsAt) return new Date(event.eventStartsAt) < now;
|
||
|
|
if (event.eventDate) return event.eventDate < today;
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Count the races on a season-standings calendar (F1, IndyCar).
|
||
|
|
*
|
||
|
|
* `event_type` has no race value, so a racing calendar is stored as
|
||
|
|
* `schedule_event` rows — the admin default for the `season_standings` scoring
|
||
|
|
* pattern. The only other row such a season carries is the single
|
||
|
|
* `final_standings` event that assigns fantasy placements once the championship
|
||
|
|
* is settled. A race is therefore "every event except `final_standings`", not
|
||
|
|
* "every event except `schedule_event`" — getting that backwards leaves the
|
||
|
|
* simulator with zero remaining races and no idea the season is in progress.
|
||
|
|
*/
|
||
|
|
export async function countSeasonRaces(
|
||
|
|
sportsSeasonId: string,
|
||
|
|
now: Date = new Date(),
|
||
|
|
providedDb?: ReturnType<typeof database>
|
||
|
|
): Promise<SeasonRaceCounts> {
|
||
|
|
const db = providedDb || database();
|
||
|
|
|
||
|
|
const events = await db.query.scoringEvents.findMany({
|
||
|
|
where: eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
|
||
|
|
columns: {
|
||
|
|
eventType: true,
|
||
|
|
isComplete: true,
|
||
|
|
eventDate: true,
|
||
|
|
eventStartsAt: true,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
const today = now.toISOString().split("T")[0];
|
||
|
|
let completed = 0;
|
||
|
|
let remaining = 0;
|
||
|
|
|
||
|
|
for (const event of events) {
|
||
|
|
if (event.eventType === "final_standings") continue;
|
||
|
|
if (hasRaceRun(event, now, today)) completed++;
|
||
|
|
else remaining++;
|
||
|
|
}
|
||
|
|
|
||
|
|
return { completed, remaining, total: completed + remaining };
|
||
|
|
}
|