2026-03-21 00:12:01 -07:00
|
|
|
import { database } from "~/database/context";
|
|
|
|
|
import * as schema from "~/database/schema";
|
2026-03-23 21:22:53 -07:00
|
|
|
import { eq, and, max, sql } from "drizzle-orm";
|
2026-03-21 00:12:01 -07:00
|
|
|
|
|
|
|
|
export interface UpsertRegularSeasonStandingData {
|
|
|
|
|
participantId: string;
|
|
|
|
|
sportsSeasonId: string;
|
|
|
|
|
wins: number;
|
|
|
|
|
losses: number;
|
|
|
|
|
otLosses?: number | null;
|
|
|
|
|
ties?: 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;
|
2026-03-26 00:34:51 -07:00
|
|
|
srs?: number | null;
|
2026-03-21 00:12:01 -07:00
|
|
|
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,
|
2026-03-21 09:44:05 -07:00
|
|
|
winPct: r.winPct !== null && r.winPct !== undefined ? r.winPct.toString() : null,
|
2026-03-21 00:12:01 -07:00
|
|
|
gamesPlayed: r.gamesPlayed,
|
2026-03-21 09:44:05 -07:00
|
|
|
gamesBack: r.gamesBack !== null && r.gamesBack !== undefined ? r.gamesBack.toString() : null,
|
2026-03-21 00:12:01 -07:00
|
|
|
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,
|
2026-03-26 00:34:51 -07:00
|
|
|
srs: r.srs !== null && r.srs !== undefined ? r.srs.toString() : null,
|
2026-03-21 00:12:01 -07:00
|
|
|
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: {
|
2026-03-23 21:22:53 -07:00
|
|
|
wins: sql`excluded.wins`,
|
|
|
|
|
losses: sql`excluded.losses`,
|
|
|
|
|
otLosses: sql`excluded.ot_losses`,
|
|
|
|
|
ties: sql`excluded.ties`,
|
|
|
|
|
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`,
|
2026-03-26 00:34:51 -07:00
|
|
|
srs: sql`excluded.srs`,
|
2026-03-23 21:22:53 -07:00
|
|
|
syncedAt: sql`excluded.synced_at`,
|
|
|
|
|
updatedAt: sql`excluded.updated_at`,
|
2026-03-21 00:12:01 -07:00
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
.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
|
2026-03-21 09:44:05 -07:00
|
|
|
return rows.toSorted((a, b) => {
|
2026-03-21 00:12:01 -07:00
|
|
|
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 },
|
|
|
|
|
});
|
|
|
|
|
}
|