Fix isScoring fallback and loserAdvances gaps across all match-processing paths
Three issues found in code review around the recalculate-floors fix: 1. set-winner and set-round-winners (bracket.server.ts) used `match.isScoring ?? true` just like recalculate-floors did. Both now derive isScoring from the bracket template as the source of truth, falling back to the DB field only when the round isn't defined in the template. 2. processPlayoffEvent (scoring-calculator.ts) used `matches[0]?.isScoring ?? true` with the same DB-default problem. Now uses BRACKET_TEMPLATES[bracketTemplateId] to look up the round's isScoring before falling back to the DB field. 3. doesLoserAdvance (playoff-match.ts) was missing the AFL afl_10 Qualifying Finals case: both losers advance to Semi-Finals. Without this, AFL QF losers would incorrectly receive finalPosition=0 when processed through the non-scoring path. https://claude.ai/code/session_01D3NbnVQdaH82Fk7Jm8y3sB
This commit is contained in:
parent
65593c82cf
commit
8d4b1eb238
3 changed files with 35 additions and 22 deletions
|
|
@ -1316,9 +1316,15 @@ export function doesLoserAdvance(
|
||||||
matchNumber: number,
|
matchNumber: number,
|
||||||
templateId: string
|
templateId: string
|
||||||
): boolean {
|
): boolean {
|
||||||
|
// NBA Play-In Round 1: 7v8 games (M1, M3) — loser gets a second chance in Round 2.
|
||||||
|
// 9v10 games (M2, M4) — loser is immediately eliminated.
|
||||||
if (templateId === "nba_20" && round === "Play-In Round 1") {
|
if (templateId === "nba_20" && round === "Play-In Round 1") {
|
||||||
return matchNumber === 1 || matchNumber === 3;
|
return matchNumber === 1 || matchNumber === 3;
|
||||||
}
|
}
|
||||||
|
// AFL Qualifying Finals: both losers (1v4 and 2v3) advance to Semi-Finals.
|
||||||
|
if (templateId === "afl_10" && round === "Qualifying Finals") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -187,9 +187,12 @@ export async function processPlayoffEvent(
|
||||||
// Process based on round
|
// Process based on round
|
||||||
const round = event.playoffRound;
|
const round = event.playoffRound;
|
||||||
|
|
||||||
// Check if this is a scoring round
|
// Prefer template-defined isScoring over the DB field: the DB column defaults to
|
||||||
// First try to get isScoring from the match (template-based)
|
// true, so legacy play-in rows that predate the column are incorrectly marked as
|
||||||
const isScoring = matches[0]?.isScoring ?? true; // Default to true for legacy brackets
|
// scoring and would assign wrong placements to losers who still have a second game.
|
||||||
|
const bracketTemplate = event.bracketTemplateId ? BRACKET_TEMPLATES[event.bracketTemplateId] : undefined;
|
||||||
|
const templateRoundDef = bracketTemplate?.rounds.find((r) => r.name === round);
|
||||||
|
const isScoring = templateRoundDef !== undefined ? templateRoundDef.isScoring : (matches[0]?.isScoring ?? true);
|
||||||
|
|
||||||
// PHASE 5.3: Handle teams that didn't make the playoffs
|
// PHASE 5.3: Handle teams that didn't make the playoffs
|
||||||
// Get all participants in the sports season
|
// Get all participants in the sports season
|
||||||
|
|
|
||||||
|
|
@ -279,33 +279,33 @@ export async function action({ request, params }: Route.ActionArgs) {
|
||||||
await setMatchWinner(matchId, winnerId, loserId);
|
await setMatchWinner(matchId, winnerId, loserId);
|
||||||
|
|
||||||
// Try to advance the winner to the next round using template
|
// Try to advance the winner to the next round using template
|
||||||
if (event.bracketTemplateId) {
|
const setWinnerTemplate = event.bracketTemplateId ? getBracketTemplate(event.bracketTemplateId) : null;
|
||||||
const template = getBracketTemplate(event.bracketTemplateId);
|
if (setWinnerTemplate) {
|
||||||
if (template) {
|
try {
|
||||||
try {
|
await advanceWinnerTemplate(matchId, winnerId, setWinnerTemplate);
|
||||||
await advanceWinnerTemplate(matchId, winnerId, template);
|
} catch (error) {
|
||||||
} catch (error) {
|
// Only ignore "already filled" errors, re-throw unexpected errors
|
||||||
// Only ignore "already filled" errors, re-throw unexpected errors
|
if (
|
||||||
if (
|
error instanceof Error &&
|
||||||
error instanceof Error &&
|
error.message.includes("already filled")
|
||||||
error.message.includes("already filled")
|
) {
|
||||||
) {
|
logger.warn("Match already filled, skipping advancement");
|
||||||
logger.warn("Match already filled, skipping advancement");
|
} else {
|
||||||
} else {
|
throw error; // Re-throw unexpected errors
|
||||||
throw error; // Re-throw unexpected errors
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Immediately score this match: loser gets their final placement,
|
// Immediately score this match: loser gets their final placement,
|
||||||
// winner gets provisional floor points (isPartialScore=true).
|
// winner gets provisional floor points (isPartialScore=true).
|
||||||
// isScoring ?? true: default to true for legacy rows pre-dating the isScoring column.
|
// Prefer the template-defined isScoring over the DB field: the DB column
|
||||||
|
// defaults to true, so legacy play-in rows may be incorrectly marked as scoring.
|
||||||
|
const setWinnerRoundIsScoring = setWinnerTemplate?.rounds.find((r) => r.name === match.round)?.isScoring;
|
||||||
await processMatchResult({
|
await processMatchResult({
|
||||||
round: match.round,
|
round: match.round,
|
||||||
winnerId,
|
winnerId,
|
||||||
loserId,
|
loserId,
|
||||||
isScoring: match.isScoring ?? true,
|
isScoring: setWinnerRoundIsScoring !== undefined ? setWinnerRoundIsScoring : (match.isScoring ?? true),
|
||||||
sportsSeasonId: event.sportsSeasonId,
|
sportsSeasonId: event.sportsSeasonId,
|
||||||
bracketTemplateId: event.bracketTemplateId,
|
bracketTemplateId: event.bracketTemplateId,
|
||||||
eventId: event.id,
|
eventId: event.id,
|
||||||
|
|
@ -356,6 +356,11 @@ export async function action({ request, params }: Route.ActionArgs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const template = event.bracketTemplateId ? getBracketTemplate(event.bracketTemplateId) : null;
|
const template = event.bracketTemplateId ? getBracketTemplate(event.bracketTemplateId) : null;
|
||||||
|
// Prefer template-defined isScoring over the DB field (DB defaults to true,
|
||||||
|
// so legacy play-in rows may be incorrectly marked as scoring).
|
||||||
|
const roundIsScoring = new Map<string, boolean>(
|
||||||
|
template?.rounds.map((r) => [r.name, r.isScoring]) ?? []
|
||||||
|
);
|
||||||
|
|
||||||
// Process each winner assignment
|
// Process each winner assignment
|
||||||
let successCount = 0;
|
let successCount = 0;
|
||||||
|
|
@ -409,12 +414,11 @@ export async function action({ request, params }: Route.ActionArgs) {
|
||||||
|
|
||||||
// Score this match without triggering standings/probability recalc yet —
|
// Score this match without triggering standings/probability recalc yet —
|
||||||
// we batch those side effects into a single call after the loop.
|
// we batch those side effects into a single call after the loop.
|
||||||
// isScoring ?? true: default to true for legacy rows pre-dating the isScoring column.
|
|
||||||
await processMatchResult({
|
await processMatchResult({
|
||||||
round: match.round,
|
round: match.round,
|
||||||
winnerId,
|
winnerId,
|
||||||
loserId,
|
loserId,
|
||||||
isScoring: match.isScoring ?? true,
|
isScoring: roundIsScoring.has(match.round) ? roundIsScoring.get(match.round)! : (match.isScoring ?? true),
|
||||||
sportsSeasonId: event.sportsSeasonId,
|
sportsSeasonId: event.sportsSeasonId,
|
||||||
bracketTemplateId: event.bracketTemplateId,
|
bracketTemplateId: event.bracketTemplateId,
|
||||||
eventName: event.name ?? undefined,
|
eventName: event.name ?? undefined,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue