2026-07-03 20:40:47 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* 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.
|
|
|
|
|
|
*
|
2026-07-03 21:51:23 +00:00
|
|
|
|
* npx tsx scripts/backfill-qp-resplit.ts # apply, all tournaments
|
|
|
|
|
|
* npx tsx scripts/backfill-qp-resplit.ts --dry # report only
|
|
|
|
|
|
* npx tsx scripts/backfill-qp-resplit.ts --name Wimbledon # scope to matching event names
|
|
|
|
|
|
* npx tsx scripts/backfill-qp-resplit.ts --name Wimbledon --sport Men # + scope to a sport (e.g. gender)
|
|
|
|
|
|
* npx tsx scripts/backfill-qp-resplit.ts --name Wimbledon --sport Men --dry
|
|
|
|
|
|
*
|
|
|
|
|
|
* --name matches case-insensitively against each linked scoring event's name;
|
|
|
|
|
|
* matches ALL tournaments with a matching event (e.g. both years of "Wimbledon"),
|
|
|
|
|
|
* since the reported bug spans seasons. Tournament names don't encode gender (both
|
|
|
|
|
|
* men's and women's majors are just "Wimbledon") — use --sport to scope further,
|
|
|
|
|
|
* matched case-insensitively against the sport's name (e.g. "Tennis - Men").
|
|
|
|
|
|
* Both filters apply to a tournament if ANY of its linked events match.
|
2026-07-03 20:40:47 +00:00
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
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");
|
2026-07-03 21:51:23 +00:00
|
|
|
|
function argValue(flag: string): string | null {
|
|
|
|
|
|
const idx = process.argv.indexOf(flag);
|
|
|
|
|
|
return idx !== -1 && process.argv[idx + 1] ? process.argv[idx + 1].toLowerCase() : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
const NAME_FILTER = argValue("--name");
|
|
|
|
|
|
const SPORT_FILTER = argValue("--sport");
|
2026-07-03 20:40:47 +00:00
|
|
|
|
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<string, typeof events>();
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-03 21:51:23 +00:00
|
|
|
|
if (NAME_FILTER) {
|
|
|
|
|
|
for (const [tournamentId, evs] of byTournament) {
|
|
|
|
|
|
const matches = evs.some((e) => e.name?.toLowerCase().includes(NAME_FILTER));
|
|
|
|
|
|
if (!matches) byTournament.delete(tournamentId);
|
|
|
|
|
|
}
|
|
|
|
|
|
log(`Filtering to tournaments matching --name "${NAME_FILTER}"`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (SPORT_FILTER) {
|
|
|
|
|
|
for (const [tournamentId, evs] of byTournament) {
|
|
|
|
|
|
const matches = evs.some((e) =>
|
|
|
|
|
|
e.sportsSeason?.sport?.name?.toLowerCase().includes(SPORT_FILTER)
|
|
|
|
|
|
);
|
|
|
|
|
|
if (!matches) byTournament.delete(tournamentId);
|
|
|
|
|
|
}
|
|
|
|
|
|
log(`Filtering to tournaments matching --sport "${SPORT_FILTER}"`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-03 20:40:47 +00:00
|
|
|
|
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);
|
2026-07-03 21:51:23 +00:00
|
|
|
|
const sportName = evs[0]?.sportsSeason?.sport?.name ?? "unknown sport";
|
|
|
|
|
|
const name = `${evs[0]?.name ?? tournamentId} [${sportName}]`;
|
2026-07-03 20:40:47 +00:00
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
});
|