brackt/app/services/brackt.server.ts

351 lines
13 KiB
TypeScript
Raw Permalink Normal View History

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;
}