brackt/app/models/regular-season-standings.ts
2026-05-07 11:48:57 -07:00

215 lines
6.9 KiB
TypeScript

import { database } from "~/database/context";
import * as schema from "~/database/schema";
import { eq, and, max, sql } from "drizzle-orm";
export interface UpsertRegularSeasonStandingData {
participantId: string;
sportsSeasonId: string;
wins: number;
losses: number;
otLosses?: number | null;
ties?: number | null;
tablePoints?: number | null;
goalsFor?: number | null;
goalsAgainst?: number | null;
goalDifference?: number | null;
winPct?: number | null;
gamesPlayed: number;
gamesBack?: number | null;
conference?: string | null;
division?: string | null;
conferenceRank?: number | null;
divisionRank?: number | null;
leagueRank?: number | null;
streak?: string | null;
lastTen?: string | null;
homeRecord?: string | null;
awayRecord?: string | null;
externalTeamId?: string | null;
srs?: number | null;
syncedAt?: Date | null;
}
/**
* Bulk upsert regular season standings records.
* Uses onConflictDoUpdate on the (participantId, sportsSeasonId) unique index.
* Sets syncedAt = now() for auto-synced records; null means manually entered.
*/
export async function upsertRegularSeasonStandings(
records: UpsertRegularSeasonStandingData[],
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
if (records.length === 0) return [];
const now = new Date();
const values = records.map((r) => ({
participantId: r.participantId,
sportsSeasonId: r.sportsSeasonId,
wins: r.wins,
losses: r.losses,
otLosses: r.otLosses ?? null,
ties: r.ties ?? null,
tablePoints: r.tablePoints ?? null,
goalsFor: r.goalsFor ?? null,
goalsAgainst: r.goalsAgainst ?? null,
goalDifference: r.goalDifference ?? null,
winPct: r.winPct !== null && r.winPct !== undefined ? r.winPct.toString() : null,
gamesPlayed: r.gamesPlayed,
gamesBack: r.gamesBack !== null && r.gamesBack !== undefined ? r.gamesBack.toString() : null,
conference: r.conference ?? null,
division: r.division ?? null,
conferenceRank: r.conferenceRank ?? null,
divisionRank: r.divisionRank ?? null,
leagueRank: r.leagueRank ?? null,
streak: r.streak ?? null,
lastTen: r.lastTen ?? null,
homeRecord: r.homeRecord ?? null,
awayRecord: r.awayRecord ?? null,
externalTeamId: r.externalTeamId ?? null,
srs: r.srs !== null && r.srs !== undefined ? r.srs.toString() : null,
syncedAt: r.syncedAt !== undefined ? r.syncedAt : now,
updatedAt: now,
}));
return await db.transaction(async (tx) => {
return tx
.insert(schema.regularSeasonStandings)
.values(values)
.onConflictDoUpdate({
target: [
schema.regularSeasonStandings.participantId,
schema.regularSeasonStandings.sportsSeasonId,
],
set: {
wins: sql`excluded.wins`,
losses: sql`excluded.losses`,
otLosses: sql`excluded.ot_losses`,
ties: sql`excluded.ties`,
tablePoints: sql`excluded.table_points`,
goalsFor: sql`excluded.goals_for`,
goalsAgainst: sql`excluded.goals_against`,
goalDifference: sql`excluded.goal_difference`,
winPct: sql`excluded.win_pct`,
gamesPlayed: sql`excluded.games_played`,
gamesBack: sql`excluded.games_back`,
conference: sql`excluded.conference`,
division: sql`excluded.division`,
conferenceRank: sql`excluded.conference_rank`,
divisionRank: sql`excluded.division_rank`,
leagueRank: sql`excluded.league_rank`,
streak: sql`excluded.streak`,
lastTen: sql`excluded.last_ten`,
homeRecord: sql`excluded.home_record`,
awayRecord: sql`excluded.away_record`,
externalTeamId: sql`excluded.external_team_id`,
srs: sql`excluded.srs`,
syncedAt: sql`excluded.synced_at`,
updatedAt: sql`excluded.updated_at`,
},
})
.returning();
});
}
/**
* Upsert a single standing record as a manual admin override.
* Sets syncedAt = null to indicate manual entry (won't conflict with auto-sync).
*/
export async function upsertManualStanding(
participantId: string,
sportsSeasonId: string,
data: Omit<UpsertRegularSeasonStandingData, "participantId" | "sportsSeasonId" | "syncedAt">,
providedDb?: ReturnType<typeof database>
) {
const results = await upsertRegularSeasonStandings(
[{ ...data, participantId, sportsSeasonId, syncedAt: null }],
providedDb
);
return results[0];
}
/**
* Get all regular season standings for a sports season.
* Ordered by conference, division, then divisionRank (or leagueRank as fallback).
*/
export async function getRegularSeasonStandings(
sportsSeasonId: string,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
const rows = await db.query.regularSeasonStandings.findMany({
where: eq(schema.regularSeasonStandings.sportsSeasonId, sportsSeasonId),
with: {
participant: true,
},
});
// Sort: conference → division → divisionRank ?? leagueRank ?? leagueRank, nulls last
return rows.toSorted((a, b) => {
const confA = a.conference ?? "ZZZ";
const confB = b.conference ?? "ZZZ";
if (confA !== confB) return confA.localeCompare(confB);
const divA = a.division ?? "ZZZ";
const divB = b.division ?? "ZZZ";
if (divA !== divB) return divA.localeCompare(divB);
const rankA = a.divisionRank ?? a.leagueRank ?? 999;
const rankB = b.divisionRank ?? b.leagueRank ?? 999;
return rankA - rankB;
});
}
/**
* Returns the most recent syncedAt timestamp across all records for a season.
* Returns null if no auto-synced records exist.
*/
export async function getLastSyncedAt(
sportsSeasonId: string,
providedDb?: ReturnType<typeof database>
): Promise<Date | null> {
const db = providedDb || database();
const result = await db
.select({ lastSync: max(schema.regularSeasonStandings.syncedAt) })
.from(schema.regularSeasonStandings)
.where(eq(schema.regularSeasonStandings.sportsSeasonId, sportsSeasonId));
return result[0]?.lastSync ?? null;
}
/**
* Delete all regular season standings for a sports season.
*/
export async function deleteRegularSeasonStandings(
sportsSeasonId: string,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
await db
.delete(schema.regularSeasonStandings)
.where(eq(schema.regularSeasonStandings.sportsSeasonId, sportsSeasonId));
}
/**
* Get a single standing record for a participant in a season.
*/
export async function getParticipantStanding(
participantId: string,
sportsSeasonId: string,
providedDb?: ReturnType<typeof database>
) {
const db = providedDb || database();
return db.query.regularSeasonStandings.findFirst({
where: and(
eq(schema.regularSeasonStandings.participantId, participantId),
eq(schema.regularSeasonStandings.sportsSeasonId, sportsSeasonId)
),
with: { participant: true },
});
}