From e3fb3cb158dc6e6defdcde07c843f38b4a4d7d37 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Thu, 14 May 2026 10:23:23 +0200 Subject: [PATCH 1/4] CC-63: Average Ranking Factors Divided by Wrong Round Count Resolves CC-63 --- .../tournamentResults/_helpers/aggregateTournamentData.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/convex/_model/tournamentResults/_helpers/aggregateTournamentData.ts b/convex/_model/tournamentResults/_helpers/aggregateTournamentData.ts index 03fab27d..a9919edb 100644 --- a/convex/_model/tournamentResults/_helpers/aggregateTournamentData.ts +++ b/convex/_model/tournamentResults/_helpers/aggregateTournamentData.ts @@ -116,6 +116,7 @@ export const aggregateTournamentData = async ( } // ---- 3. Convert stats to ranking factors for players and competitors ---- + const roundsPlayed = round + 1; // Add 1 to get length rather than index return { registrations: Object.entries(registrationStats).map(([key, { results }]) => { const id = key as Id<'tournamentRegistrations'>; @@ -123,7 +124,7 @@ export const aggregateTournamentData = async ( id, gamesPlayed: results.length, opponentIds: Array.from(new Set(results.map((s) => s.opponentId))).filter((id) => id !== null), - rankingFactors: computeRankingFactors(id, registrationStats, defaultBaseStats, round), + rankingFactors: computeRankingFactors(id, registrationStats, defaultBaseStats, roundsPlayed), rank: -1, }; }), @@ -139,7 +140,7 @@ export const aggregateTournamentData = async ( opponentIds: Array.from(new Set(results.map((s) => s.opponentId))).filter((id) => id !== null), playedTables: Array.from(playedTables), byeRounds: Array.from(byeRounds), - rankingFactors: computeRankingFactors(id, competitorStats, defaultBaseStats, round), + rankingFactors: computeRankingFactors(id, competitorStats, defaultBaseStats, roundsPlayed), rank: -1, }; }), From 733e16baf759187eb2d5bd2a42600cb0e3170090 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Fri, 15 May 2026 07:58:54 +0200 Subject: [PATCH 2/4] Ensure change in tournament config updates rankings --- convex/_model/tournaments/triggers.ts | 37 +++++++++++++++++++++++++++ convex/functions.ts | 1 + 2 files changed, 38 insertions(+) diff --git a/convex/_model/tournaments/triggers.ts b/convex/_model/tournaments/triggers.ts index e1c9e7e3..527cc65a 100644 --- a/convex/_model/tournaments/triggers.ts +++ b/convex/_model/tournaments/triggers.ts @@ -1,6 +1,43 @@ +import isEqual from 'fast-deep-equal/es6'; + import { MutationCtx } from '../../_generated/server'; import { TriggerChange } from '../common/types'; import { refreshLeagueRankings as refreshLeagueRankingsHandler } from '../leagues'; +import { refreshTournamentResult } from '../tournamentResults'; + +/** + * Trigger refresh of tournament results when ranking factors change. + * + * Only refreshes when fields that affect ranking change: + * - rankingFactors: directly affects ranking factors + */ +export const refreshTournamentResults = async ( + ctx: MutationCtx, + change: TriggerChange<'tournaments'>, +): Promise => { + const { newDoc, oldDoc } = change; + + if (!newDoc || !oldDoc) { + return; + } + + // For updates, skip if no ranking-relevant fields changed: + if (isEqual(newDoc.rankingFactors, oldDoc.rankingFactors)) { + return; + } + + if (newDoc.status === 'archived') { + return; + } + + const tournamentResults = await ctx.db.query('tournamentResults') + .withIndex('by_tournament_round', (q) => q.eq('tournamentId', change.id)) + .collect(); + + for (const tournamentResult of tournamentResults) { + await refreshTournamentResult(ctx, tournamentResult); + } +}; /** * Trigger refresh of league rankings when a tournament is archived. diff --git a/convex/functions.ts b/convex/functions.ts index c0c0203f..5b69fc04 100644 --- a/convex/functions.ts +++ b/convex/functions.ts @@ -78,3 +78,4 @@ triggers.register('tournamentCompetitors', tournamentCompetitorTriggers.refreshT triggers.register('tournamentRegistrations', tournamentRegistrationTriggers.refreshTournamentResults); triggers.register('tournamentResults', tournamentResultTriggers.refreshLeagueRankings); triggers.register('tournaments', tournamentTriggers.refreshLeagueRankings); +triggers.register('tournaments', tournamentTriggers.refreshTournamentResults); From 3a480123e9302350b355886272cb78a698f163a9 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Fri, 15 May 2026 07:59:59 +0200 Subject: [PATCH 3/4] Update all tournaments --- convex/migrations.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/convex/migrations.ts b/convex/migrations.ts index 0e19046a..48314e6f 100644 --- a/convex/migrations.ts +++ b/convex/migrations.ts @@ -50,17 +50,14 @@ export const fixMissingListData = migrations.define({ }, }); -export const migrateTournamentResults = migrations.define({ +export const refreshTournamentResults = migrations.define({ table: 'tournaments', migrateOne: async (ctx, doc) => { - if (doc.status === 'archived') { - const rounds = doc.lastRound !== undefined ? doc.lastRound + 1 : doc.roundCount; - for (let i = 0; i < rounds; i++) { - await refreshTournamentResult(ctx, { - tournamentId: doc._id, - round: i, - }); - } + const tournamentResults = await ctx.db.query('tournamentResults') + .withIndex('by_tournament_round', (q) => q.eq('tournamentId', doc._id)) + .collect(); + for (const result of tournamentResults) { + await refreshTournamentResult(ctx, result); } }, }); From ff115e1bca03633ca5bc4ed23928c09ff6aeb0f7 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Fri, 15 May 2026 08:26:44 +0200 Subject: [PATCH 4/4] PR Feedback --- convex/_model/tournaments/triggers.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/convex/_model/tournaments/triggers.ts b/convex/_model/tournaments/triggers.ts index 527cc65a..ef92b529 100644 --- a/convex/_model/tournaments/triggers.ts +++ b/convex/_model/tournaments/triggers.ts @@ -9,7 +9,7 @@ import { refreshTournamentResult } from '../tournamentResults'; * Trigger refresh of tournament results when ranking factors change. * * Only refreshes when fields that affect ranking change: - * - rankingFactors: directly affects ranking factors + * - rankingFactors */ export const refreshTournamentResults = async ( ctx: MutationCtx, @@ -21,12 +21,12 @@ export const refreshTournamentResults = async ( return; } - // For updates, skip if no ranking-relevant fields changed: - if (isEqual(newDoc.rankingFactors, oldDoc.rankingFactors)) { + if (newDoc.status === 'draft' || newDoc.status === 'published') { return; } - if (newDoc.status === 'archived') { + // For updates, skip if no ranking-relevant fields changed: + if (isEqual(newDoc.rankingFactors, oldDoc.rankingFactors)) { return; }