9.5 KiB
League Standings Calculation Analysis
Overview
This document analyzes the standings calculation logic for the GridPilot leagues feature. The system has two different standings entities:
- Standing (core/racing/domain/entities/Standing.ts) - League-level standings
- 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.
async recalculate(leagueId: string): Promise<Standing[]> {
// ...
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.
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.
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.
export class Standing extends Entity<string> {
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.
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.
export interface LeagueStandingsRepository {
getLeagueStandings(leagueId: string): Promise<RawStanding[]>;
}
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.
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:
-
League-level standings (
Standingentity):- Stored per league (not per season)
- Updated immediately when races complete
- Used by
GetLeagueStandingsUseCase - Don't support season-specific features
-
Season-level standings (
ChampionshipStandingentity):- 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:
CompleteRaceUseCaseandImportRaceResultsUseCaseupdate league-level standingsRecalculateChampionshipStandingsUseCasecalculates season-level standingsGetLeagueStandingsUseCaseretrieves league-level standings
Recommendations
Short-term Fixes
-
Fix CompleteRaceUseCase to use league's points system
- Retrieve points system from league configuration
- Apply correct points based on league settings
-
Fix ImportRaceResultsUseCase to recalculate only relevant standings
- Pass seasonId to recalculate method
- Only recalculate standings for the specific season
-
Add seasonId to Standing entity
- Modify Standing entity to include seasonId
- Update all repositories and use cases to handle seasonId
-
Fix GetLeagueStandingsUseCase to accept seasonId parameter
- Add seasonId parameter to input
- Filter standings by seasonId
Long-term Refactoring
-
Consolidate standings into single system
- Merge Standing and ChampionshipStanding entities
- Use ChampionshipStanding as the primary standings entity
- Remove league-level standings
-
Update CompleteRaceUseCase to use RecalculateChampionshipStandingsUseCase
- Instead of updating standings immediately, trigger recalculation
- Ensure season-specific calculations
-
Update ImportRaceResultsUseCase to use RecalculateChampionshipStandingsUseCase
- Instead of calling StandingRepository.recalculate(), call RecalculateChampionshipStandingsUseCase
- Ensure season-specific calculations
-
Update GetLeagueStandingsUseCase to retrieve ChampionshipStanding
- Change to retrieve season-specific standings
- Add seasonId parameter to input
-
Remove LeagueStandingsRepository
- This repository is not connected to standing calculation
- Use ChampionshipStandingRepository instead
-
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:
- League-level standings don't track seasons
- Points systems are hardcoded in some places
- Standings recalculation includes all seasons
- 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.