# League Standings Calculation Analysis ## Overview This document analyzes the standings calculation logic for the GridPilot leagues feature. The system has two different standings entities: 1. **Standing** (core/racing/domain/entities/Standing.ts) - League-level standings 2. **ChampionshipStanding** (core/racing/domain/entities/championship/ChampionshipStanding.ts) - Season-level standings ## Architecture Overview ### Data Flow ``` Race Completion/Result Import ↓ CompleteRaceUseCase / ImportRaceResultsUseCase ↓ StandingRepository.recalculate() or StandingRepository.save() ↓ Standing Entity (League-level) ↓ GetLeagueStandingsUseCase ↓ LeagueStandingsPresenter ↓ API Response ``` ### Season-Level Standings Flow ``` Season Completion ↓ RecalculateChampionshipStandingsUseCase ↓ ChampionshipStanding Entity (Season-level) ↓ ChampionshipStandingRepository ``` ## Identified Issues ### Issue 1: InMemoryStandingRepository.recalculate() Doesn't Consider Seasons **Location:** `adapters/racing/persistence/inmemory/InMemoryStandingRepository.ts:169-270` **Problem:** The `recalculate()` method calculates standings from ALL completed races in a league, not just races from a specific season. ```typescript async recalculate(leagueId: string): Promise { // ... const races = await this.raceRepository.findCompletedByLeagueId(leagueId); // ... } ``` **Impact:** When standings are recalculated, they include results from all seasons, not just the current/active season. This means: - Historical season results are mixed with current season results - Standings don't accurately reflect a specific season's performance - Drop score policies can't be applied correctly per season **Expected Behavior:** Standings should be calculated per season, considering only races from that specific season. ### Issue 2: CompleteRaceUseCase Uses Hardcoded Points System **Location:** `core/racing/application/use-cases/CompleteRaceUseCase.ts:200-203` **Problem:** The `updateStandings()` method uses a hardcoded F1 points system instead of the league's configured points system. ```typescript standing = standing.addRaceResult(result.position.toNumber(), { 1: 25, 2: 18, 3: 15, 4: 12, 5: 10, 6: 8, 7: 6, 8: 4, 9: 2, 10: 1 }); ``` **Impact:** - All leagues use the same points system regardless of their configuration - Custom points systems are ignored - IndyCar and other racing series points systems are not applied **Expected Behavior:** The points system should be retrieved from the league's configuration and applied accordingly. ### Issue 3: ImportRaceResultsUseCase Recalculates All League Standings **Location:** `core/racing/application/use-cases/ImportRaceResultsUseCase.ts:156` **Problem:** When importing race results, the method calls `standingRepository.recalculate(league.id.toString())` which recalculates ALL standings for the league. ```typescript await this.standingRepository.recalculate(league.id.toString()); ``` **Impact:** - Performance issue: Recalculating all standings when only one race is imported - Standings include results from all seasons, not just the current season - Inefficient for large leagues with many races **Expected Behavior:** Only recalculate standings for the specific season that the race belongs to. ### Issue 4: Standing Entity Doesn't Track Season Association **Location:** `core/racing/domain/entities/Standing.ts` **Problem:** The `Standing` entity only tracks `leagueId` and `driverId`, but not `seasonId`. ```typescript export class Standing extends Entity { readonly leagueId: LeagueId; readonly driverId: DriverId; // No seasonId field } ``` **Impact:** - Standings are stored per league, not per season - Can't distinguish between standings from different seasons - Can't apply season-specific drop score policies **Expected Behavior:** Standing should include a `seasonId` field to track which season the standings belong to. ### Issue 5: GetLeagueStandingsUseCase Retrieves League-Level Standings **Location:** `core/racing/application/use-cases/GetLeagueStandingsUseCase.ts:39` **Problem:** The use case retrieves standings from `StandingRepository.findByLeagueId()`, which returns league-level standings. ```typescript const standings = await this.standingRepository.findByLeagueId(input.leagueId); ``` **Impact:** - Returns standings that include results from all seasons - Doesn't provide season-specific standings - Can't apply season-specific drop score policies **Expected Behavior:** The use case should retrieve season-specific standings (ChampionshipStanding) or accept a seasonId parameter. ### Issue 6: LeagueStandingsRepository is Not Connected to Standing Calculation **Location:** `adapters/racing/persistence/inmemory/InMemoryLeagueStandingsRepository.ts` **Problem:** The `LeagueStandingsRepository` interface is defined but not connected to the standing calculation logic. ```typescript export interface LeagueStandingsRepository { getLeagueStandings(leagueId: string): Promise; } ``` **Impact:** - The repository is not used by any standing calculation use case - It's only used for storing pre-calculated standings - No clear connection between standing calculation and this repository **Expected Behavior:** The repository should be used to store and retrieve season-specific standings. ### Issue 7: LeagueStandingsPresenter Hardcodes Metrics **Location:** `apps/api/src/domain/league/presenters/LeagueStandingsPresenter.ts:24-30` **Problem:** The presenter hardcodes wins, podiums, and races metrics to 0. ```typescript wins: 0, podiums: 0, races: 0, ``` **Impact:** - API always returns 0 for these metrics - Users can't see actual win/podium/race counts - TODO comment indicates this is a known issue **Expected Behavior:** These metrics should be calculated and returned from the standing entity. ## Root Cause Analysis The core issue is a **design inconsistency** between two different standings systems: 1. **League-level standings** (`Standing` entity): - Stored per league (not per season) - Updated immediately when races complete - Used by `GetLeagueStandingsUseCase` - Don't support season-specific features 2. **Season-level standings** (`ChampionshipStanding` entity): - Stored per season - Calculated using `RecalculateChampionshipStandingsUseCase` - Support drop score policies per season - Not used by the main standings API The system has **two parallel standings calculation paths** that don't align: - `CompleteRaceUseCase` and `ImportRaceResultsUseCase` update league-level standings - `RecalculateChampionshipStandingsUseCase` calculates season-level standings - `GetLeagueStandingsUseCase` retrieves league-level standings ## Recommendations ### Short-term Fixes 1. **Fix CompleteRaceUseCase to use league's points system** - Retrieve points system from league configuration - Apply correct points based on league settings 2. **Fix ImportRaceResultsUseCase to recalculate only relevant standings** - Pass seasonId to recalculate method - Only recalculate standings for the specific season 3. **Add seasonId to Standing entity** - Modify Standing entity to include seasonId - Update all repositories and use cases to handle seasonId 4. **Fix GetLeagueStandingsUseCase to accept seasonId parameter** - Add seasonId parameter to input - Filter standings by seasonId ### Long-term Refactoring 1. **Consolidate standings into single system** - Merge Standing and ChampionshipStanding entities - Use ChampionshipStanding as the primary standings entity - Remove league-level standings 2. **Update CompleteRaceUseCase to use RecalculateChampionshipStandingsUseCase** - Instead of updating standings immediately, trigger recalculation - Ensure season-specific calculations 3. **Update ImportRaceResultsUseCase to use RecalculateChampionshipStandingsUseCase** - Instead of calling StandingRepository.recalculate(), call RecalculateChampionshipStandingsUseCase - Ensure season-specific calculations 4. **Update GetLeagueStandingsUseCase to retrieve ChampionshipStanding** - Change to retrieve season-specific standings - Add seasonId parameter to input 5. **Remove LeagueStandingsRepository** - This repository is not connected to standing calculation - Use ChampionshipStandingRepository instead 6. **Fix LeagueStandingsPresenter to return actual metrics** - Calculate wins, podiums, and races from standing entity - Remove hardcoded values ## Testing Strategy ### Unit Tests - Test CompleteRaceUseCase with different points systems - Test ImportRaceResultsUseCase with season-specific recalculation - Test GetLeagueStandingsUseCase with seasonId parameter - Test Standing entity with seasonId field ### Integration Tests - Test complete race flow with season-specific standings - Test import race results flow with season-specific standings - Test standings recalculation for specific season - Test API endpoints with season-specific standings ### End-to-End Tests - Test league standings API with season parameter - Test standings calculation across multiple seasons - Test drop score policy application per season ## Conclusion The standings calculation system has significant design issues that prevent accurate season-specific standings. The main problems are: 1. League-level standings don't track seasons 2. Points systems are hardcoded in some places 3. Standings recalculation includes all seasons 4. Two parallel standings systems don't align Fixing these issues requires both short-term fixes and long-term refactoring to consolidate the standings system into a single, season-aware design.