Fix code-review findings on qualifying-bracket QP scoring
- #1 (correctness): processQualifyingEvent regrouped event_results by placement using the live group size as the tie count. The "Process Qualifying Points" button could run mid-bracket and average four placement-3 QF winners down to 7 QP instead of their 9 floor. processQualifyingEvent now delegates bracket events to processQualifyingBracketEvent (structural tie spans) and skips the live-count regroup. finalize-bracket simplified to call processQualifyingEvent. - #2: reprocess-bracket on a finalized qualifying season re-runs finalizeQualifyingPoints so the deleted final placements are restored. - #3: replace direct Drizzle deletes in the route with the existing deleteParticipantResultsBySportsSeasonId model helper. - #4: extract shared writeEventResultsQP helper; assignCs2EliminationQP and processQualifyingBracketEvent both use it instead of duplicating the upsert + recalculateParticipantQP loop. - #5: extract scoreQualifyingBracket route helper for the repeated branch. - #6: document that qualifying brackets intentionally skip the fantasy Recent Scores feed (they surface via QP standings + Discord); scope the set-winner Discord notification to the entered match. Adds a regression test locking the structural-tieCount contract (9 QP, not 7). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QVLqaYGXCeSYYqTHZmbRZi
This commit is contained in:
parent
50f2f93b0b
commit
5dfe39bba2
6 changed files with 185 additions and 129 deletions
|
|
@ -170,6 +170,25 @@ describe("guaranteed-minimum QP per outcome (default config)", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Regression guard: structural tieCount, not live count (the #1 fix) ─────────
|
||||||
|
|
||||||
|
describe("structural tie span vs. live-count regrouping", () => {
|
||||||
|
it("4 QF winners share placement 3 but each keeps the 2-slot (9 QP) floor", () => {
|
||||||
|
const s = states(QF_ALL);
|
||||||
|
const winners = ["t1", "t2", "t3", "t4"].map((id) => s.get(id)!);
|
||||||
|
// All four sit at placement 3, but with the STRUCTURAL tie span of 2.
|
||||||
|
for (const w of winners) {
|
||||||
|
expect(w).toEqual({ placement: 3, tieCount: 2 });
|
||||||
|
expect(qpFor(w.placement, w.tieCount)).toBeCloseTo(9);
|
||||||
|
}
|
||||||
|
// A naive regroup by LIVE count (4 rows at placement 3) would average over
|
||||||
|
// slots 3–6 and wrongly yield 7 QP — the bug processQualifyingEvent must avoid
|
||||||
|
// by delegating bracket events to the structural-tieCount path.
|
||||||
|
expect(qpFor(3, 4)).toBeCloseTo(7);
|
||||||
|
expect(qpFor(3, 4)).not.toBeCloseTo(9);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ── Regression guard: reused ROUND_CONFIG floors are intact ────────────────────
|
// ── Regression guard: reused ROUND_CONFIG floors are intact ────────────────────
|
||||||
|
|
||||||
describe("ROUND_CONFIG floors (regression guard)", () => {
|
describe("ROUND_CONFIG floors (regression guard)", () => {
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,9 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { database } from "~/database/context";
|
import { database } from "~/database/context";
|
||||||
import { cs2MajorStageResults, seasonParticipants, eventResults } from "~/database/schema";
|
import { cs2MajorStageResults, seasonParticipants } from "~/database/schema";
|
||||||
import { eq, and, sql } from "drizzle-orm";
|
import { eq, and, sql } from "drizzle-orm";
|
||||||
import { getQPConfig, recalculateParticipantQP, calculateSplitQualifyingPoints } from "~/models/qualifying-points";
|
import { getQPConfig, calculateSplitQualifyingPoints, writeEventResultsQP } from "~/models/qualifying-points";
|
||||||
|
|
||||||
export interface Cs2StageResult {
|
export interface Cs2StageResult {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -394,34 +394,5 @@ export async function assignCs2EliminationQP(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date();
|
await writeEventResultsQP(scoringEventId, sportsSeasonId, resultsByParticipant, db);
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
[...resultsByParticipant.entries()].map(([seasonParticipantId, { qp, placement }]) =>
|
|
||||||
db
|
|
||||||
.insert(eventResults)
|
|
||||||
.values({
|
|
||||||
scoringEventId,
|
|
||||||
seasonParticipantId,
|
|
||||||
qualifyingPointsAwarded: qp.toString(),
|
|
||||||
placement,
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
})
|
|
||||||
.onConflictDoUpdate({
|
|
||||||
target: [eventResults.scoringEventId, eventResults.seasonParticipantId],
|
|
||||||
set: {
|
|
||||||
qualifyingPointsAwarded: qp.toString(),
|
|
||||||
placement,
|
|
||||||
updatedAt: now,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
[...resultsByParticipant.keys()].map((seasonParticipantId) =>
|
|
||||||
recalculateParticipantQP(seasonParticipantId, sportsSeasonId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,9 +95,10 @@ export async function deleteParticipantResult(id: string): Promise<void> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteParticipantResultsBySportsSeasonId(
|
export async function deleteParticipantResultsBySportsSeasonId(
|
||||||
sportsSeasonId: string
|
sportsSeasonId: string,
|
||||||
|
providedDb?: ReturnType<typeof database>
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const db = database();
|
const db = providedDb || database();
|
||||||
await db
|
await db
|
||||||
.delete(schema.seasonParticipantResults)
|
.delete(schema.seasonParticipantResults)
|
||||||
.where(eq(schema.seasonParticipantResults.sportsSeasonId, sportsSeasonId));
|
.where(eq(schema.seasonParticipantResults.sportsSeasonId, sportsSeasonId));
|
||||||
|
|
|
||||||
|
|
@ -283,6 +283,57 @@ export async function recalculateParticipantQP(
|
||||||
return { totalQP, eventsScored };
|
return { totalQP, eventsScored };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upsert qualifying points + placement into event_results for a set of participants,
|
||||||
|
* then recalculate each participant's QP total.
|
||||||
|
*
|
||||||
|
* Shared by the QP writers (assignCs2EliminationQP for Swiss-stage exits,
|
||||||
|
* processQualifyingBracketEvent for Champions-Stage / knockout brackets) so the
|
||||||
|
* upsert + recalc loop lives in one place. Upserts on
|
||||||
|
* (scoringEventId, seasonParticipantId), so repeated calls overwrite cleanly.
|
||||||
|
*
|
||||||
|
* @param entries Map from seasonParticipantId → { qp, placement } to write.
|
||||||
|
*/
|
||||||
|
export async function writeEventResultsQP(
|
||||||
|
scoringEventId: string,
|
||||||
|
sportsSeasonId: string,
|
||||||
|
entries: Map<string, { qp: number; placement: number }>,
|
||||||
|
providedDb?: ReturnType<typeof database>
|
||||||
|
): Promise<void> {
|
||||||
|
if (entries.size === 0) return;
|
||||||
|
const db = providedDb || database();
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[...entries.entries()].map(([seasonParticipantId, { qp, placement }]) =>
|
||||||
|
db
|
||||||
|
.insert(schema.eventResults)
|
||||||
|
.values({
|
||||||
|
scoringEventId,
|
||||||
|
seasonParticipantId,
|
||||||
|
qualifyingPointsAwarded: qp.toFixed(2),
|
||||||
|
placement,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: [schema.eventResults.scoringEventId, schema.eventResults.seasonParticipantId],
|
||||||
|
set: {
|
||||||
|
qualifyingPointsAwarded: qp.toFixed(2),
|
||||||
|
placement,
|
||||||
|
updatedAt: now,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[...entries.keys()].map((seasonParticipantId) =>
|
||||||
|
recalculateParticipantQP(seasonParticipantId, sportsSeasonId, db)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recalculate eventsScored for a participant by counting unique events with QP awarded
|
* Recalculate eventsScored for a participant by counting unique events with QP awarded
|
||||||
* @deprecated Use recalculateParticipantQP instead for more accurate recalculation
|
* @deprecated Use recalculateParticipantQP instead for more accurate recalculation
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import {
|
||||||
getQPConfig,
|
getQPConfig,
|
||||||
hasProcessedQualifyingPlacement,
|
hasProcessedQualifyingPlacement,
|
||||||
recalculateParticipantQP,
|
recalculateParticipantQP,
|
||||||
|
writeEventResultsQP,
|
||||||
getQPStandings,
|
getQPStandings,
|
||||||
updateFinalRankings,
|
updateFinalRankings,
|
||||||
} from "./qualifying-points";
|
} from "./qualifying-points";
|
||||||
|
|
@ -749,11 +750,13 @@ export function deriveBracketQualifyingStates(
|
||||||
* qualifying_points sport the per-major fantasy placement is meaningless — final
|
* qualifying_points sport the per-major fantasy placement is meaningless — final
|
||||||
* fantasy placements come solely from finalizeQualifyingPoints across all majors.
|
* fantasy placements come solely from finalizeQualifyingPoints across all majors.
|
||||||
*
|
*
|
||||||
* Invariant: progressive QP totals come from recalculateParticipantQP summing the
|
* QP is written directly with the placement tier's STRUCTURAL tie span (QF tier
|
||||||
* directly-written qualifyingPointsAwarded — never from interim re-grouping.
|
* spans 4 slots, SF tier 2, finalist/champion 1) — not the live count of teams
|
||||||
* processQualifyingEvent's placement-grouping recompute only runs at finalization,
|
* currently at a placement. This is what makes a QF-winner floor 9 QP (avg of
|
||||||
* by which point the bracket is complete and the placement groups have their
|
* 3rd+4th) even while all four QF winners transiently share placement 3.
|
||||||
* structural sizes, so both paths produce identical QP.
|
* processQualifyingEvent delegates to this function for bracket events precisely
|
||||||
|
* so it does NOT re-group those rows by live count (which would average four
|
||||||
|
* placement-3 rows down to 7).
|
||||||
*
|
*
|
||||||
* Idempotent: re-running on the same match state recomputes identical values.
|
* Idempotent: re-running on the same match state recomputes identical values.
|
||||||
*/
|
*/
|
||||||
|
|
@ -799,37 +802,14 @@ export async function processQualifyingBracketEvent(
|
||||||
qpConfigArray.map((c) => [c.placement, parseFloat(c.points)])
|
qpConfigArray.map((c) => [c.placement, parseFloat(c.points)])
|
||||||
);
|
);
|
||||||
|
|
||||||
const now = new Date();
|
const entries = new Map<string, { qp: number; placement: number }>(
|
||||||
|
[...states.entries()].map(([id, { placement, tieCount }]) => [
|
||||||
await Promise.all(
|
id,
|
||||||
[...states.entries()].map(([seasonParticipantId, { placement, tieCount }]) => {
|
{ qp: calculateSplitQualifyingPoints(placement, tieCount, qpMap), placement },
|
||||||
const qp = calculateSplitQualifyingPoints(placement, tieCount, qpMap);
|
])
|
||||||
return db
|
|
||||||
.insert(schema.eventResults)
|
|
||||||
.values({
|
|
||||||
scoringEventId: eventId,
|
|
||||||
seasonParticipantId,
|
|
||||||
qualifyingPointsAwarded: qp.toFixed(2),
|
|
||||||
placement,
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
})
|
|
||||||
.onConflictDoUpdate({
|
|
||||||
target: [schema.eventResults.scoringEventId, schema.eventResults.seasonParticipantId],
|
|
||||||
set: {
|
|
||||||
qualifyingPointsAwarded: qp.toFixed(2),
|
|
||||||
placement,
|
|
||||||
updatedAt: now,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await Promise.all(
|
await writeEventResultsQP(eventId, event.sportsSeasonId, entries, db);
|
||||||
[...states.keys()].map((seasonParticipantId) =>
|
|
||||||
recalculateParticipantQP(seasonParticipantId, event.sportsSeasonId, db)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -864,52 +844,62 @@ export async function processQualifyingEvent(
|
||||||
// Check if this was already processed (for majorsCompleted counter)
|
// Check if this was already processed (for majorsCompleted counter)
|
||||||
const wasAlreadyProcessed = hasProcessedQualifyingPlacement(results);
|
const wasAlreadyProcessed = hasProcessedQualifyingPlacement(results);
|
||||||
|
|
||||||
// Clear stale QP before reprocessing so removed/null placements do not keep old awards.
|
if (event.bracketTemplateId) {
|
||||||
for (const result of results) {
|
// Bracket-based qualifying event (e.g. CS2 Champions Stage): QP is owned by the
|
||||||
await db
|
// bracket/stage writers, which assign each placement its STRUCTURAL tie span.
|
||||||
.update(schema.eventResults)
|
// Re-derive via processQualifyingBracketEvent and leave the Swiss-exit rows
|
||||||
.set({
|
// (written by assignCs2EliminationQP) untouched. This deliberately skips the
|
||||||
qualifyingPointsAwarded: "0",
|
// placement-regroup below — re-grouping provisional floors by their LIVE count
|
||||||
updatedAt: new Date(),
|
// would average four placement-3 QF winners down to 7 QP instead of their 9 floor.
|
||||||
})
|
await processQualifyingBracketEvent(eventId, db);
|
||||||
.where(eq(schema.eventResults.id, result.id));
|
} else {
|
||||||
}
|
// Clear stale QP before reprocessing so removed/null placements do not keep old awards.
|
||||||
|
for (const result of results) {
|
||||||
const qpConfig = await getQPConfig(event.sportsSeasonId, db);
|
|
||||||
const pointsByPlacement = new Map(
|
|
||||||
qpConfig.map((config) => [config.placement, parseFloat(config.points)])
|
|
||||||
);
|
|
||||||
|
|
||||||
// Group results by placement to handle ties
|
|
||||||
const placementGroups = new Map<number, typeof results>();
|
|
||||||
for (const result of results) {
|
|
||||||
if (result.placement) {
|
|
||||||
const group = placementGroups.get(result.placement) || [];
|
|
||||||
group.push(result);
|
|
||||||
placementGroups.set(result.placement, group);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process each placement group and update event_results with QP awarded
|
|
||||||
for (const [placement, group] of placementGroups) {
|
|
||||||
const tieCount = group.length;
|
|
||||||
|
|
||||||
const qpPerParticipant = calculateSplitQualifyingPoints(
|
|
||||||
placement,
|
|
||||||
tieCount,
|
|
||||||
pointsByPlacement
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update the event result with the QP awarded
|
|
||||||
for (const result of group) {
|
|
||||||
await db
|
await db
|
||||||
.update(schema.eventResults)
|
.update(schema.eventResults)
|
||||||
.set({
|
.set({
|
||||||
qualifyingPointsAwarded: qpPerParticipant.toFixed(2),
|
qualifyingPointsAwarded: "0",
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
})
|
})
|
||||||
.where(eq(schema.eventResults.id, result.id));
|
.where(eq(schema.eventResults.id, result.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const qpConfig = await getQPConfig(event.sportsSeasonId, db);
|
||||||
|
const pointsByPlacement = new Map(
|
||||||
|
qpConfig.map((config) => [config.placement, parseFloat(config.points)])
|
||||||
|
);
|
||||||
|
|
||||||
|
// Group results by placement to handle ties
|
||||||
|
const placementGroups = new Map<number, typeof results>();
|
||||||
|
for (const result of results) {
|
||||||
|
if (result.placement) {
|
||||||
|
const group = placementGroups.get(result.placement) || [];
|
||||||
|
group.push(result);
|
||||||
|
placementGroups.set(result.placement, group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process each placement group and update event_results with QP awarded
|
||||||
|
for (const [placement, group] of placementGroups) {
|
||||||
|
const tieCount = group.length;
|
||||||
|
|
||||||
|
const qpPerParticipant = calculateSplitQualifyingPoints(
|
||||||
|
placement,
|
||||||
|
tieCount,
|
||||||
|
pointsByPlacement
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update the event result with the QP awarded
|
||||||
|
for (const result of group) {
|
||||||
|
await db
|
||||||
|
.update(schema.eventResults)
|
||||||
|
.set({
|
||||||
|
qualifyingPointsAwarded: qpPerParticipant.toFixed(2),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(schema.eventResults.id, result.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recalculate totals for all participants from scratch
|
// Recalculate totals for all participants from scratch
|
||||||
|
|
|
||||||
|
|
@ -27,13 +27,18 @@ import {
|
||||||
processMatchResult,
|
processMatchResult,
|
||||||
processQualifyingBracketEvent,
|
processQualifyingBracketEvent,
|
||||||
processQualifyingEvent,
|
processQualifyingEvent,
|
||||||
|
finalizeQualifyingPoints,
|
||||||
recalculateAffectedLeagues,
|
recalculateAffectedLeagues,
|
||||||
recalculateStandings,
|
recalculateStandings,
|
||||||
autoCompleteRoundIfDone,
|
autoCompleteRoundIfDone,
|
||||||
} from "~/models/scoring-calculator";
|
} from "~/models/scoring-calculator";
|
||||||
import { updateProbabilitiesAfterResult } from "~/services/probability-updater";
|
import { updateProbabilitiesAfterResult } from "~/services/probability-updater";
|
||||||
import { getBracketTemplate, ALL_16_SEEDS, type BracketRegion } from "~/lib/bracket-templates";
|
import { getBracketTemplate, ALL_16_SEEDS, type BracketRegion } from "~/lib/bracket-templates";
|
||||||
import { setParticipantResult, findParticipantResultsBySportsSeasonId } from "~/models/participant-result";
|
import {
|
||||||
|
setParticipantResult,
|
||||||
|
findParticipantResultsBySportsSeasonId,
|
||||||
|
deleteParticipantResultsBySportsSeasonId,
|
||||||
|
} from "~/models/participant-result";
|
||||||
import { findSeasonSportsBySportsSeasonId } from "~/models/season-sport";
|
import { findSeasonSportsBySportsSeasonId } from "~/models/season-sport";
|
||||||
import { createDailySnapshot } from "~/models/standings";
|
import { createDailySnapshot } from "~/models/standings";
|
||||||
import {
|
import {
|
||||||
|
|
@ -105,6 +110,28 @@ export async function loader({ params }: Route.LoaderArgs) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Score a qualifying-event bracket (e.g. CS2 Champions Stage): derive each team's
|
||||||
|
* guaranteed-minimum QP from the bracket and refresh affected league standings.
|
||||||
|
*
|
||||||
|
* Qualifying brackets award QUALIFYING POINTS, not fantasy points, so this never
|
||||||
|
* writes seasonParticipantResults and never records team_score_events — match
|
||||||
|
* results surface via the QP Standings and the Discord standings update. Final
|
||||||
|
* fantasy placements come from finalizeQualifyingPoints across all majors.
|
||||||
|
*/
|
||||||
|
async function scoreQualifyingBracket(
|
||||||
|
event: { id: string; sportsSeasonId: string; name: string | null },
|
||||||
|
db: ReturnType<typeof database>,
|
||||||
|
recalcOptions?: Parameters<typeof recalculateAffectedLeagues>[2]
|
||||||
|
): Promise<void> {
|
||||||
|
await processQualifyingBracketEvent(event.id, db);
|
||||||
|
await recalculateAffectedLeagues(
|
||||||
|
event.sportsSeasonId,
|
||||||
|
db,
|
||||||
|
recalcOptions ?? { eventId: event.id, eventName: event.name ?? undefined }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function action({ request, params }: Route.ActionArgs) {
|
export async function action({ request, params }: Route.ActionArgs) {
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const intent = formData.get("intent");
|
const intent = formData.get("intent");
|
||||||
|
|
@ -273,12 +300,11 @@ export async function action({ request, params }: Route.ActionArgs) {
|
||||||
|
|
||||||
if (event.isQualifyingEvent) {
|
if (event.isQualifyingEvent) {
|
||||||
// Qualifying major (e.g. CS2): bracket results award QUALIFYING POINTS, not
|
// Qualifying major (e.g. CS2): bracket results award QUALIFYING POINTS, not
|
||||||
// fantasy points. Derive each team's guaranteed-minimum QP floor from the
|
// fantasy points. matchIds scopes the Discord notification to just this match.
|
||||||
// bracket and write it to event_results — do NOT touch seasonParticipantResults.
|
await scoreQualifyingBracket(event, db, {
|
||||||
await processQualifyingBracketEvent(event.id, db);
|
|
||||||
await recalculateAffectedLeagues(event.sportsSeasonId, db, {
|
|
||||||
eventId: event.id,
|
eventId: event.id,
|
||||||
eventName: event.name ?? undefined,
|
eventName: event.name ?? undefined,
|
||||||
|
matchIds: [matchId],
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Immediately score this match: loser gets their final placement,
|
// Immediately score this match: loser gets their final placement,
|
||||||
|
|
@ -504,12 +530,7 @@ export async function action({ request, params }: Route.ActionArgs) {
|
||||||
// processQualifyingBracketEvent — there are no seasonParticipantResults rows to
|
// processQualifyingBracketEvent — there are no seasonParticipantResults rows to
|
||||||
// validate against, and final fantasy placements come from finalizeQualifyingPoints.
|
// validate against, and final fantasy placements come from finalizeQualifyingPoints.
|
||||||
if (event.isQualifyingEvent) {
|
if (event.isQualifyingEvent) {
|
||||||
const qpDb = database();
|
await scoreQualifyingBracket(event, database());
|
||||||
await processQualifyingBracketEvent(params.eventId, qpDb);
|
|
||||||
await recalculateAffectedLeagues(event.sportsSeasonId, qpDb, {
|
|
||||||
eventId: params.eventId,
|
|
||||||
eventName: event.name ?? undefined,
|
|
||||||
});
|
|
||||||
return { success: `${round} completed and qualifying points updated` };
|
return { success: `${round} completed and qualifying points updated` };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -605,12 +626,17 @@ export async function action({ request, params }: Route.ActionArgs) {
|
||||||
// finalizeQualifyingPoints across all majors.
|
// finalizeQualifyingPoints across all majors.
|
||||||
if (event.isQualifyingEvent) {
|
if (event.isQualifyingEvent) {
|
||||||
const db = database();
|
const db = database();
|
||||||
await db
|
await deleteParticipantResultsBySportsSeasonId(event.sportsSeasonId, db);
|
||||||
.delete(schema.seasonParticipantResults)
|
|
||||||
.where(eq(schema.seasonParticipantResults.sportsSeasonId, event.sportsSeasonId));
|
|
||||||
await processQualifyingBracketEvent(params.eventId, db);
|
await processQualifyingBracketEvent(params.eventId, db);
|
||||||
// skipDiscord: reprocess is a data-correction tool, not a result announcement.
|
// If the season's QP was already finalized, the delete above wiped the final
|
||||||
await recalculateAffectedLeagues(event.sportsSeasonId, db, { skipDiscord: true });
|
// placements — recompute them from QP totals so standings aren't left blank.
|
||||||
|
const sportsSeason = await findSportsSeasonById(params.id);
|
||||||
|
if (sportsSeason?.qualifyingPointsFinalized) {
|
||||||
|
await finalizeQualifyingPoints(event.sportsSeasonId, db); // recalcs leagues itself
|
||||||
|
} else {
|
||||||
|
// skipDiscord: reprocess is a data-correction tool, not a result announcement.
|
||||||
|
await recalculateAffectedLeagues(event.sportsSeasonId, db, { skipDiscord: true });
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
success: `Reprocessed qualifying bracket: cleared stale fantasy points and recomputed QP (${completed.length} completed match(es)).`,
|
success: `Reprocessed qualifying bracket: cleared stale fantasy points and recomputed QP (${completed.length} completed match(es)).`,
|
||||||
};
|
};
|
||||||
|
|
@ -624,11 +650,7 @@ export async function action({ request, params }: Route.ActionArgs) {
|
||||||
// Only deleting partial rows leaves stale finalized rows that block
|
// Only deleting partial rows leaves stale finalized rows that block
|
||||||
// the "never un-finalize" guard in upsertParticipantResult.
|
// the "never un-finalize" guard in upsertParticipantResult.
|
||||||
const db = database();
|
const db = database();
|
||||||
await db
|
await deleteParticipantResultsBySportsSeasonId(event.sportsSeasonId, db);
|
||||||
.delete(schema.seasonParticipantResults)
|
|
||||||
.where(
|
|
||||||
eq(schema.seasonParticipantResults.sportsSeasonId, event.sportsSeasonId)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Replay each completed match in bracket order (earlier rounds first).
|
// Replay each completed match in bracket order (earlier rounds first).
|
||||||
const template = event.bracketTemplateId ? getBracketTemplate(event.bracketTemplateId) : null;
|
const template = event.bracketTemplateId ? getBracketTemplate(event.bracketTemplateId) : null;
|
||||||
|
|
@ -732,7 +754,9 @@ export async function action({ request, params }: Route.ActionArgs) {
|
||||||
// fantasy placements come from finalizeQualifyingPoints across all of them.
|
// fantasy placements come from finalizeQualifyingPoints across all of them.
|
||||||
if (event.isQualifyingEvent) {
|
if (event.isQualifyingEvent) {
|
||||||
const db = database();
|
const db = database();
|
||||||
await processQualifyingBracketEvent(params.eventId, db);
|
// processQualifyingEvent derives the bracket QP (via processQualifyingBracketEvent),
|
||||||
|
// increments majorsCompleted, and recalcs participant QP totals. Season-wide
|
||||||
|
// fantasy finalization stays with finalizeQualifyingPoints across all majors.
|
||||||
await processQualifyingEvent(params.eventId, db);
|
await processQualifyingEvent(params.eventId, db);
|
||||||
await db
|
await db
|
||||||
.update(schema.scoringEvents)
|
.update(schema.scoringEvents)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue