351 lines
13 KiB
TypeScript
351 lines
13 KiB
TypeScript
|
|
import { and, eq, inArray, isNull } from "drizzle-orm";
|
||
|
|
import { database } from "~/database/context";
|
||
|
|
import * as schema from "~/database/schema";
|
||
|
|
import { batchUpsertParticipantEVs, syncVorpForSeason } from "~/models/participant-expected-value";
|
||
|
|
import { recalculateStandings } from "~/models/scoring-calculator";
|
||
|
|
import { createDailySnapshot } from "~/models/standings";
|
||
|
|
import { BracktSimulator } from "~/services/simulations/brackt-simulator";
|
||
|
|
|
||
|
|
export const BRACKT_SLUG = "brackt";
|
||
|
|
|
||
|
|
type Db = ReturnType<typeof database>;
|
||
|
|
|
||
|
|
function isBracktSport(sport: { slug: string } | null | undefined) {
|
||
|
|
return sport?.slug === BRACKT_SLUG;
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function seedOrRepairBracktTemplate(providedDb?: Db) {
|
||
|
|
const db = providedDb || database();
|
||
|
|
return await db.transaction(async (tx) => {
|
||
|
|
const existingSport = await tx.query.sports.findFirst({
|
||
|
|
where: eq(schema.sports.slug, BRACKT_SLUG),
|
||
|
|
});
|
||
|
|
|
||
|
|
const [sport] = existingSport
|
||
|
|
? await tx
|
||
|
|
.update(schema.sports)
|
||
|
|
.set({ name: "Brackt", type: "team", simulatorType: "brackt", updatedAt: new Date() })
|
||
|
|
.where(eq(schema.sports.id, existingSport.id))
|
||
|
|
.returning()
|
||
|
|
: await tx
|
||
|
|
.insert(schema.sports)
|
||
|
|
.values({
|
||
|
|
name: "Brackt",
|
||
|
|
type: "team",
|
||
|
|
slug: BRACKT_SLUG,
|
||
|
|
description: "League meta-sport based on final Brackt standings",
|
||
|
|
simulatorType: "brackt",
|
||
|
|
})
|
||
|
|
.returning();
|
||
|
|
|
||
|
|
const existingTemplate = await tx.query.sportsSeasons.findFirst({
|
||
|
|
where: and(eq(schema.sportsSeasons.sportId, sport.id), isNull(schema.sportsSeasons.fantasySeasonId)),
|
||
|
|
orderBy: (sportsSeasons, { desc }) => [desc(sportsSeasons.year)],
|
||
|
|
});
|
||
|
|
|
||
|
|
const templateValues = {
|
||
|
|
sportId: sport.id,
|
||
|
|
name: "Brackt",
|
||
|
|
year: 2026,
|
||
|
|
startDate: "2026-01-01",
|
||
|
|
endDate: "2099-12-31",
|
||
|
|
status: "upcoming" as const,
|
||
|
|
scoringType: "regular_season" as const,
|
||
|
|
scoringPattern: "season_standings" as const,
|
||
|
|
draftOn: "2026-01-01",
|
||
|
|
draftOff: "2099-12-31",
|
||
|
|
fantasySeasonId: null,
|
||
|
|
updatedAt: new Date(),
|
||
|
|
};
|
||
|
|
|
||
|
|
const [template] = existingTemplate
|
||
|
|
? await tx
|
||
|
|
.update(schema.sportsSeasons)
|
||
|
|
.set(templateValues)
|
||
|
|
.where(eq(schema.sportsSeasons.id, existingTemplate.id))
|
||
|
|
.returning()
|
||
|
|
: await tx.insert(schema.sportsSeasons).values(templateValues).returning();
|
||
|
|
|
||
|
|
return { sport, template };
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function findPrivateBracktSeason(fantasySeasonId: string, providedDb?: Db) {
|
||
|
|
const db = providedDb || database();
|
||
|
|
return db.query.sportsSeasons.findFirst({
|
||
|
|
where: eq(schema.sportsSeasons.fantasySeasonId, fantasySeasonId),
|
||
|
|
with: { sport: true },
|
||
|
|
}).then((season) => (isBracktSport(season?.sport) ? season : undefined));
|
||
|
|
}
|
||
|
|
|
||
|
|
async function ensurePrivateBracktSeason(fantasySeasonId: string, templateId: string, db: Db) {
|
||
|
|
const existing = await findPrivateBracktSeason(fantasySeasonId, db);
|
||
|
|
if (existing) return existing;
|
||
|
|
|
||
|
|
const template = await db.query.sportsSeasons.findFirst({
|
||
|
|
where: eq(schema.sportsSeasons.id, templateId),
|
||
|
|
with: { sport: true },
|
||
|
|
});
|
||
|
|
if (!template || !isBracktSport(template.sport)) {
|
||
|
|
throw new Error("Brackt template sports season not found");
|
||
|
|
}
|
||
|
|
|
||
|
|
const fantasySeason = await db.query.seasons.findFirst({
|
||
|
|
where: eq(schema.seasons.id, fantasySeasonId),
|
||
|
|
with: { league: true },
|
||
|
|
});
|
||
|
|
|
||
|
|
const [privateSeason] = await db
|
||
|
|
.insert(schema.sportsSeasons)
|
||
|
|
.values({
|
||
|
|
sportId: template.sportId,
|
||
|
|
fantasySeasonId,
|
||
|
|
name: `${fantasySeason?.league.name ?? "League"} Brackt`,
|
||
|
|
year: fantasySeason?.year ?? template.year,
|
||
|
|
startDate: template.startDate,
|
||
|
|
endDate: template.endDate,
|
||
|
|
status: "upcoming",
|
||
|
|
scoringType: "regular_season",
|
||
|
|
scoringPattern: "season_standings",
|
||
|
|
draftOn: template.draftOn,
|
||
|
|
draftOff: template.draftOff,
|
||
|
|
})
|
||
|
|
.returning();
|
||
|
|
|
||
|
|
return { ...privateSeason, sport: template.sport };
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function syncPrivateBracktParticipants(fantasySeasonId: string, providedDb?: Db) {
|
||
|
|
const db = providedDb || database();
|
||
|
|
const bracktSeason = await findPrivateBracktSeason(fantasySeasonId, db);
|
||
|
|
if (!bracktSeason) return null;
|
||
|
|
|
||
|
|
const teams = await db.query.teams.findMany({
|
||
|
|
where: eq(schema.teams.seasonId, fantasySeasonId),
|
||
|
|
orderBy: (teamsTable, { asc }) => [asc(teamsTable.name)],
|
||
|
|
});
|
||
|
|
const participants = await db.query.seasonParticipants.findMany({
|
||
|
|
where: eq(schema.seasonParticipants.sportsSeasonId, bracktSeason.id),
|
||
|
|
});
|
||
|
|
|
||
|
|
const participantByTeamId = new Map(participants.map((p) => [p.externalId, p]));
|
||
|
|
const teamIds = new Set(teams.map((team) => team.id));
|
||
|
|
|
||
|
|
for (const team of teams) {
|
||
|
|
const participant = participantByTeamId.get(team.id);
|
||
|
|
if (participant) {
|
||
|
|
if (participant.name !== team.name || participant.shortName !== team.name) {
|
||
|
|
await db
|
||
|
|
.update(schema.seasonParticipants)
|
||
|
|
.set({ name: team.name, shortName: team.name, updatedAt: new Date() })
|
||
|
|
.where(eq(schema.seasonParticipants.id, participant.id));
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
await db.insert(schema.seasonParticipants).values({
|
||
|
|
sportsSeasonId: bracktSeason.id,
|
||
|
|
name: team.name,
|
||
|
|
shortName: team.name,
|
||
|
|
externalId: team.id,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const obsolete = participants.filter((p) => p.externalId && !teamIds.has(p.externalId));
|
||
|
|
if (obsolete.length > 0) {
|
||
|
|
await db
|
||
|
|
.delete(schema.seasonParticipants)
|
||
|
|
.where(inArray(schema.seasonParticipants.id, obsolete.map((p) => p.id)));
|
||
|
|
}
|
||
|
|
|
||
|
|
await runBracktHarvilleForFantasySeason(fantasySeasonId, db);
|
||
|
|
return bracktSeason;
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function applyBracktSportsForSeason(
|
||
|
|
fantasySeasonId: string,
|
||
|
|
selectedSportsSeasonIds: string[],
|
||
|
|
providedDb?: Db
|
||
|
|
): Promise<string[]> {
|
||
|
|
const db = providedDb || database();
|
||
|
|
const selected = [...new Set(selectedSportsSeasonIds)];
|
||
|
|
if (selected.length === 0) return selected;
|
||
|
|
|
||
|
|
const sportsSeasons = await db.query.sportsSeasons.findMany({
|
||
|
|
where: inArray(schema.sportsSeasons.id, selected),
|
||
|
|
with: { sport: true },
|
||
|
|
});
|
||
|
|
const bracktTemplate = sportsSeasons.find((ss) => isBracktSport(ss.sport) && ss.fantasySeasonId === null);
|
||
|
|
const existingPrivate = await findPrivateBracktSeason(fantasySeasonId, db);
|
||
|
|
const nonBracktIds = selected.filter((id) => {
|
||
|
|
const ss = sportsSeasons.find((s) => s.id === id);
|
||
|
|
return !ss || !isBracktSport(ss.sport);
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!bracktTemplate && existingPrivate && !selected.includes(existingPrivate.id)) {
|
||
|
|
await db.delete(schema.sportsSeasons).where(eq(schema.sportsSeasons.id, existingPrivate.id));
|
||
|
|
return nonBracktIds;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!bracktTemplate) return selected;
|
||
|
|
|
||
|
|
const privateSeason = await ensurePrivateBracktSeason(fantasySeasonId, bracktTemplate.id, db);
|
||
|
|
await syncPrivateBracktParticipants(fantasySeasonId, db);
|
||
|
|
return [...nonBracktIds, privateSeason.id];
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function runBracktHarvilleForFantasySeason(fantasySeasonId: string, db: Db) {
|
||
|
|
const bracktSeason = await findPrivateBracktSeason(fantasySeasonId, db);
|
||
|
|
if (!bracktSeason) return [];
|
||
|
|
|
||
|
|
const fantasySeason = await db.query.seasons.findFirst({
|
||
|
|
where: eq(schema.seasons.id, fantasySeasonId),
|
||
|
|
});
|
||
|
|
if (!fantasySeason) return [];
|
||
|
|
|
||
|
|
const simulator = new BracktSimulator(db);
|
||
|
|
const results = await simulator.simulate(bracktSeason.id);
|
||
|
|
await batchUpsertParticipantEVs(
|
||
|
|
results.map((result) => ({
|
||
|
|
participantId: result.participantId,
|
||
|
|
sportsSeasonId: bracktSeason.id,
|
||
|
|
probabilities: result.probabilities,
|
||
|
|
scoringRules: {
|
||
|
|
pointsFor1st: fantasySeason.pointsFor1st,
|
||
|
|
pointsFor2nd: fantasySeason.pointsFor2nd,
|
||
|
|
pointsFor3rd: fantasySeason.pointsFor3rd,
|
||
|
|
pointsFor4th: fantasySeason.pointsFor4th,
|
||
|
|
pointsFor5th: fantasySeason.pointsFor5th,
|
||
|
|
pointsFor6th: fantasySeason.pointsFor6th,
|
||
|
|
pointsFor7th: fantasySeason.pointsFor7th,
|
||
|
|
pointsFor8th: fantasySeason.pointsFor8th,
|
||
|
|
},
|
||
|
|
source: "performance_model",
|
||
|
|
})),
|
||
|
|
db
|
||
|
|
);
|
||
|
|
await syncVorpForSeason(bracktSeason.id, db);
|
||
|
|
|
||
|
|
const participants = await db.query.seasonParticipants.findMany({
|
||
|
|
where: eq(schema.seasonParticipants.sportsSeasonId, bracktSeason.id),
|
||
|
|
});
|
||
|
|
return participants.map((p) => ({
|
||
|
|
participantId: p.id,
|
||
|
|
expectedValue: Number(p.expectedValue ?? 0),
|
||
|
|
vorpValue: Number(p.vorpValue ?? 0),
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
async function resolveBracktForFantasySeason(
|
||
|
|
fantasySeasonId: string,
|
||
|
|
db: Db,
|
||
|
|
trigger: "auto" | "manual",
|
||
|
|
completedSportsSeasonId?: string
|
||
|
|
) {
|
||
|
|
const bracktSeason = await findPrivateBracktSeason(fantasySeasonId, db);
|
||
|
|
if (!bracktSeason) return { status: "skipped" as const, reason: "no_brackt" };
|
||
|
|
|
||
|
|
const picks = await db.query.draftPicks.findMany({
|
||
|
|
where: eq(schema.draftPicks.seasonId, fantasySeasonId),
|
||
|
|
with: { participant: { with: { sportsSeason: { with: { sport: true } }, results: true } } },
|
||
|
|
});
|
||
|
|
const nonBracktPicks = picks.filter((pick) => !isBracktSport(pick.participant.sportsSeason.sport));
|
||
|
|
const ready = nonBracktPicks.every((pick) => {
|
||
|
|
const hasFinalizedResult = pick.participant.results.some(
|
||
|
|
(r) => r.finalPosition !== null && !r.isPartialScore
|
||
|
|
);
|
||
|
|
return hasFinalizedResult || pick.participant.sportsSeason.status === "completed";
|
||
|
|
});
|
||
|
|
if (!ready) return { status: "skipped" as const, reason: "incomplete" };
|
||
|
|
|
||
|
|
const [standings, bracktParticipants, fantasySeason] = await Promise.all([
|
||
|
|
db.query.teamStandings.findMany({ where: eq(schema.teamStandings.seasonId, fantasySeasonId) }),
|
||
|
|
db.query.seasonParticipants.findMany({ where: eq(schema.seasonParticipants.sportsSeasonId, bracktSeason.id) }),
|
||
|
|
db.query.seasons.findFirst({ where: eq(schema.seasons.id, fantasySeasonId), with: { league: true } }),
|
||
|
|
]);
|
||
|
|
if (standings.length === 0 || !fantasySeason) {
|
||
|
|
return { status: "skipped" as const, reason: "missing_standings" };
|
||
|
|
}
|
||
|
|
|
||
|
|
const participantByTeamId = new Map(bracktParticipants.map((p) => [p.externalId, p]));
|
||
|
|
const desired = standings.map((standing) => {
|
||
|
|
const participant = participantByTeamId.get(standing.teamId);
|
||
|
|
if (!participant) throw new Error(`Missing Brackt participant for team ${standing.teamId}`);
|
||
|
|
return { participantId: participant.id, finalPosition: standing.currentRank };
|
||
|
|
});
|
||
|
|
|
||
|
|
const existingResults = await db.query.seasonParticipantResults.findMany({
|
||
|
|
where: eq(schema.seasonParticipantResults.sportsSeasonId, bracktSeason.id),
|
||
|
|
});
|
||
|
|
if (existingResults.length > 0) {
|
||
|
|
const existingByParticipantId = new Map(existingResults.map((r) => [r.participantId, r]));
|
||
|
|
const differs = desired.some((placement) => {
|
||
|
|
const existing = existingByParticipantId.get(placement.participantId);
|
||
|
|
return existing && existing.finalPosition !== placement.finalPosition;
|
||
|
|
});
|
||
|
|
if (differs) {
|
||
|
|
return { status: "blocked" as const, reason: "existing_results_differ" };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const now = new Date();
|
||
|
|
const existingByParticipantId = new Map(existingResults.map((r) => [r.participantId, r]));
|
||
|
|
for (const placement of desired) {
|
||
|
|
const existing = existingByParticipantId.get(placement.participantId);
|
||
|
|
if (existing) {
|
||
|
|
await db
|
||
|
|
.update(schema.seasonParticipantResults)
|
||
|
|
.set({
|
||
|
|
finalPosition: placement.finalPosition,
|
||
|
|
isPartialScore: false,
|
||
|
|
updatedAt: now,
|
||
|
|
})
|
||
|
|
.where(eq(schema.seasonParticipantResults.id, existing.id));
|
||
|
|
} else {
|
||
|
|
await db.insert(schema.seasonParticipantResults).values({
|
||
|
|
participantId: placement.participantId,
|
||
|
|
sportsSeasonId: bracktSeason.id,
|
||
|
|
finalPosition: placement.finalPosition,
|
||
|
|
isPartialScore: false,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
await db
|
||
|
|
.update(schema.sportsSeasons)
|
||
|
|
.set({ status: "completed", updatedAt: now })
|
||
|
|
.where(eq(schema.sportsSeasons.id, bracktSeason.id));
|
||
|
|
await recalculateStandings(fantasySeasonId, db);
|
||
|
|
await createDailySnapshot(fantasySeasonId, db);
|
||
|
|
await db.insert(schema.commissionerAuditLog).values({
|
||
|
|
seasonId: fantasySeasonId,
|
||
|
|
leagueId: fantasySeason.leagueId,
|
||
|
|
actorUserId: trigger === "auto" ? "system" : "commissioner",
|
||
|
|
actorDisplayName: trigger === "auto" ? "System" : "Commissioner",
|
||
|
|
action: "brackt_resolved",
|
||
|
|
affectedTeamIds: standings.map((s) => s.teamId),
|
||
|
|
details: { trigger, completedSportsSeasonId },
|
||
|
|
});
|
||
|
|
|
||
|
|
return { status: "resolved" as const };
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function maybeResolveCompletedBracktForSportsSeason(completedSportsSeasonId: string, providedDb?: Db) {
|
||
|
|
const db = providedDb || database();
|
||
|
|
const completedSeason = await db.query.sportsSeasons.findFirst({
|
||
|
|
where: eq(schema.sportsSeasons.id, completedSportsSeasonId),
|
||
|
|
with: { sport: true },
|
||
|
|
});
|
||
|
|
if (!completedSeason || isBracktSport(completedSeason.sport)) return [];
|
||
|
|
|
||
|
|
const links = await db.query.seasonSports.findMany({
|
||
|
|
where: eq(schema.seasonSports.sportsSeasonId, completedSportsSeasonId),
|
||
|
|
});
|
||
|
|
|
||
|
|
const results = [];
|
||
|
|
for (const link of links) {
|
||
|
|
results.push(await resolveBracktForFantasySeason(link.seasonId, db, "auto", completedSportsSeasonId));
|
||
|
|
}
|
||
|
|
return results;
|
||
|
|
}
|
||
|
|
|