/** * Repair: re-seed AFL Wildcard winners into the Elimination Finals they belong in. * * Brackets advanced before the re-seeding fix crossed each Wildcard winner into a fixed * Elimination Final — the 7v10 winner always met 6th and the 8v9 winner always met 5th — * instead of pairing them by ladder position (5th hosts the lower-ranked winner). The * fix only changes how new results advance, so an already-advanced bracket keeps its * wrong pairings until this runs. The admin UI cannot re-trigger it: a completed match * renders as "Complete", with no way to re-submit the winner. * * This runs the same reseedAflEliminationFinals the advancement path now uses, so it * makes exactly the correction a fresh bracket would have. It only ever moves Wildcard * teams between the two Elimination Final slots — no results, scores or placements are * touched, and nothing else in the bracket is written. A bracket that is already correct * is left alone. * * If an Elimination Final has already been played, its qualifier cannot be moved without * rewriting who contested a recorded result; the script reports that event and skips it. * Clear and regenerate that bracket in Admin instead, then Reprocess Bracket. * * Safe to re-run. Validate on a DB snapshot first. Reads DATABASE_URL. * * npx tsx scripts/fix-afl-wildcard-reseed.ts # apply to every afl_10 event * npx tsx scripts/fix-afl-wildcard-reseed.ts --dry # report only * npx tsx scripts/fix-afl-wildcard-reseed.ts --event # one event */ import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import { eq } from "drizzle-orm"; import * as schema from "../database/schema.js"; import { DatabaseContext, database } from "../database/context.js"; import { findPlayoffMatchesByEventIdAndRound, reseedAflEliminationFinals, } from "../app/models/playoff-match.js"; import { findParticipantsBySportsSeasonId } from "../app/models/season-participant.js"; const DRY = process.argv.includes("--dry"); const eventFlag = process.argv.indexOf("--event"); const ONLY_EVENT = eventFlag === -1 ? null : process.argv[eventFlag + 1]; const log = (...a: unknown[]) => console.log(...a); async function run() { const db = database(); const events = await db.query.scoringEvents.findMany({ where: eq(schema.scoringEvents.bracketTemplateId, "afl_10"), }); const targets = ONLY_EVENT ? events.filter((e) => e.id === ONLY_EVENT) : events; if (ONLY_EVENT && targets.length === 0) { log(`No afl_10 event with id ${ONLY_EVENT}.`); return; } log(`afl_10 events to check: ${targets.length}`); let fixed = 0; let alreadyRight = 0; let skipped = 0; for (const event of targets) { const name = event.name ?? event.id; const participants = await findParticipantsBySportsSeasonId(event.sportsSeasonId); const nameOf = (id: string | null) => id === null ? "TBD" : participants.find((p) => p.id === id)?.name ?? id; /** "M1: vs " for both Elimination Finals. */ const pairings = async () => { const efMatches = await findPlayoffMatchesByEventIdAndRound(event.id, "Elimination Finals"); return efMatches .toSorted((a, b) => a.matchNumber - b.matchNumber) .map((m) => `M${m.matchNumber}: ${nameOf(m.participant1Id)} vs ${nameOf(m.participant2Id)}`) .join(", "); }; try { const before = await pairings(); // The dry run still resolves the pairings — it just reports them instead of writing. if (DRY) { const efMatches = await findPlayoffMatchesByEventIdAndRound(event.id, "Elimination Finals"); const wcMatches = await findPlayoffMatchesByEventIdAndRound(event.id, "Wildcard Round"); const decided = wcMatches.filter((m) => m.isComplete && m.winnerId).length; log(` ${name}: ${before} (${decided}/${wcMatches.length} Wildcard results, ` + `${efMatches.filter((m) => m.isComplete).length} Elimination Final(s) played)`); continue; } const reseed = await reseedAflEliminationFinals(event.id); if (reseed.vacated.length === 0 && reseed.filled.length === 0) { alreadyRight += 1; log(` = ${name}: already correct — ${before}`); continue; } fixed += 1; log(` ~ ${name}:`); log(` was: ${before}`); log(` now: ${await pairings()}`); } catch (e) { skipped += 1; log(` ! ${name}: ${(e as Error).message}`); } } if (DRY) { log("\nDry run — no writes. Re-run without --dry to apply."); return; } log(`\nDone. re-seeded=${fixed}, already correct=${alreadyRight}, skipped=${skipped}.`); } async function main() { const dbUrl = process.env.DATABASE_URL; if (!dbUrl) { console.error("ERROR: DATABASE_URL is required"); process.exit(1); } const client = postgres(dbUrl, { max: 1 }); const db = drizzle(client, { schema }); try { await DatabaseContext.run(db, run); } finally { await client.end(); } } main().catch((e) => { console.error(e); process.exit(1); });