/** * Backfill: re-score qualifying majors so mis-split QP is corrected. * * Sibling/mirror windows used to compute a placement's tie span from the players * present on THAT window's roster (a subset of the field), so a tied group split * fewer ways and over-awarded — tennis Round-of-16 losers landed at 2 QP instead of * the correct 1.5 (positions 9–16: (2+2+2+2+1+1+1+1)/8). processQualifyingEvent now * derives the tie span from the canonical full field, so re-running the fan-out * rewrites the stored event_results QP, participant totals, and league standings. * * This is idempotent and SILENT: notifications are suppressed so re-scoring history * (2 → 1.5 for many participants) does not re-ping every league's Discord. It does * NOT re-link events or designate primaries — run backfill-major-linking.ts first if * the data predates the shared-major model. majorsCompleted needs no fix (it is now * derived on read from completed qualifying events). * * Safe to re-run. Validate on a DB snapshot first. Reads DATABASE_URL. * * npx tsx scripts/backfill-qp-resplit.ts # apply * npx tsx scripts/backfill-qp-resplit.ts --dry # report only */ 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 { isBracketMajor } from "../app/lib/event-utils.js"; import { syncTournamentResults, syncMajorFromPrimaryEvent, } from "../app/services/sync-tournament-results.js"; const DRY = process.argv.includes("--dry"); const log = (...a: unknown[]) => console.log(...a); async function run() { const db = database(); // Every tournament-linked qualifying event, grouped by canonical tournament. const events = await db.query.scoringEvents.findMany({ where: eq(schema.scoringEvents.isQualifyingEvent, true), with: { sportsSeason: { with: { sport: true } } }, }); const byTournament = new Map(); for (const ev of events) { if (!ev.tournamentId) continue; // standalone/manual events: nothing to fan out const arr = byTournament.get(ev.tournamentId) ?? []; arr.push(ev); byTournament.set(ev.tournamentId, arr); } log(`Tournaments with linked qualifying events: ${byTournament.size}`); let ok = 0; let failed = 0; for (const [tournamentId, evs] of byTournament) { const simulatorType = evs[0]?.sportsSeason?.sport?.simulatorType ?? null; const bracketMajor = isBracketMajor(simulatorType); const name = evs[0]?.name ?? tournamentId; // Only re-score fully-scored majors so we don't mark in-progress ones complete. const anyComplete = evs.some((e) => e.isComplete); const markComplete = evs.every((e) => e.isComplete); if (DRY) { log( ` (dry) ${name}: ${evs.length} window(s), bracket=${bracketMajor}, ` + `complete=${markComplete} — would re-score silently` ); continue; } try { if (bracketMajor) { // Re-derive canonical from the primary, then fan out to siblings (which now // split ties by the full field). Primary is scored in place and skipped. const primary = evs.find((e) => e.isPrimary); if (!primary) { log(` ! ${name}: no primary window — skipping (run backfill-major-linking.ts)`); failed += 1; continue; } const report = await syncMajorFromPrimaryEvent(primary.id, { markComplete, skipNotifications: true, }); ok += report.windowsSynced; failed += report.windowsFailed; log( ` ${name}: re-scored via primary — ${report.windowsSynced} ok, ${report.windowsFailed} failed` ); } else { // Golf/placement: canonical tournament_results already exist; just fan out. if (!anyComplete) { log(` ${name}: no completed window — skipping`); continue; } const report = await syncTournamentResults(tournamentId, { markComplete, skipNotifications: true, }); ok += report.windowsSynced; failed += report.windowsFailed; log( ` ${name}: re-scored — ${report.windowsSynced} ok, ${report.windowsFailed} failed` ); } } catch (e) { failed += 1; log(` ! ${name}: ${(e as Error).message}`); } } log(`\nDone${DRY ? " (dry run — no writes)" : ""}. windows ok=${ok}, failed=${failed}.`); } 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); });