From 07991770991fb707764a327c1b47c9633bd5fbd9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 11 Jun 2026 20:49:35 +0000 Subject: [PATCH] Fix divisionRank ordering and add warning for unknown teams Sort teams by win-loss within each division group before assigning divisionRank so that the leader is always the best-record team, not whichever team happened to appear first in ESPN's flat response. Also warn to console when a team's displayName isn't in the hardcoded MLB_TEAM_DIVISION map, surfacing renames or expansion teams immediately instead of silently storing division="" and recreating the display bug. https://claude.ai/code/session_01288dkXYwKJUfXrEhPd8rmo --- .../standings-sync/__tests__/mlb.test.ts | 45 +++++++++++++++++-- app/services/standings-sync/mlb.ts | 20 ++++++--- 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/app/services/standings-sync/__tests__/mlb.test.ts b/app/services/standings-sync/__tests__/mlb.test.ts index 39e94a0..7eb8ab6 100644 --- a/app/services/standings-sync/__tests__/mlb.test.ts +++ b/app/services/standings-sync/__tests__/mlb.test.ts @@ -417,13 +417,50 @@ describe("MlbStandingsAdapter", () => { expect(rays?.conference).toBe("AL"); expect(braves?.conference).toBe("NL"); - // divisionRank should be 1 for each division leader, not just the globally first team - // Rays and Yankees share AL East; Rays appears first → divisionRank 1 - expect(rays?.divisionRank).toBe(1); - expect(yankees?.divisionRank).toBe(2); + // divisionRank is sorted by win-loss within each division, NOT by ESPN response order. + // Yankees (41-26) has a better record than Rays (40-25) even though Rays appears first + // in ESPN's flat list → Yankees should be divisionRank 1. + expect(yankees?.divisionRank).toBe(1); + expect(rays?.divisionRank).toBe(2); // White Sox is alone in AL Central sample → divisionRank 1 expect(whiteSox?.divisionRank).toBe(1); // Braves is alone in NL East sample → divisionRank 1 expect(braves?.divisionRank).toBe(1); }); + + it("warns to console when a team display name is not in the hardcoded division map", async () => { + const unknownTeamResponse = { + children: [ + { + name: "American League", + standings: { + entries: [ + { + team: { id: "99", displayName: "Portland Lumberjacks" }, + stats: [ + { name: "wins", value: 30 }, + { name: "losses", value: 30 }, + { name: "winPercent", value: 0.5 }, + ], + }, + ], + }, + }, + ], + }; + + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + json: async () => unknownTeamResponse, + } as Response); + + const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const adapter = new MlbStandingsAdapter(); + await adapter.fetchStandings(); + + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining("Portland Lumberjacks") + ); + warnSpy.mockRestore(); + }); }); diff --git a/app/services/standings-sync/mlb.ts b/app/services/standings-sync/mlb.ts index 6f2473e..1537ead 100644 --- a/app/services/standings-sync/mlb.ts +++ b/app/services/standings-sync/mlb.ts @@ -83,16 +83,22 @@ export class MlbStandingsAdapter implements StandingsSyncAdapter { // Pre-build statsMap once per entry so the sort doesn't rebuild it on every comparison. // Resolve division: use ESPN's value when present; fall back to the hardcoded map. - const withSm = flattened.map(({ entry, conference, division }) => ({ - entry, - conference, - division: division || MLB_TEAM_DIVISION[entry.team.displayName] || "", - sm: statsMap(entry.stats), - })); + const withSm = flattened.map(({ entry, conference, division }) => { + const resolvedDivision = division || MLB_TEAM_DIVISION[entry.team.displayName] || ""; + if (!resolvedDivision) { + console.warn( + `[MLB standings] No division found for team "${entry.team.displayName}" (id=${entry.team.id}). ` + + "Update MLB_TEAM_DIVISION if this is a renamed or expansion team." + ); + } + return { entry, conference, division: resolvedDivision, sm: statsMap(entry.stats) }; + }); // Compute divisionRank: rank each team within its own division by win/loss. // Key includes conference to prevent AL/NL teams from sharing a group when ESPN // returns short names like "East" for both leagues, or no names at all. + // Sort by win/loss within each group so rank 1 is always the best record, + // regardless of the order ESPN returns teams in the response. const divisionRankMap = new Map(); const divisionGroups = new Map(); for (const item of withSm) { @@ -101,7 +107,7 @@ export class MlbStandingsAdapter implements StandingsSyncAdapter { divisionGroups.get(key)?.push(item); } for (const divEntries of divisionGroups.values()) { - // Use the ESPN entry order within each division — ESPN already applies correct tiebreakers. + divEntries.sort(sortByWinLoss); divEntries.forEach((e, i) => { divisionRankMap.set(e.entry.team.id, i + 1); });