274 lines
9.5 KiB
Markdown
274 lines
9.5 KiB
Markdown
# 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<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.
|
|
|
|
```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<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.
|
|
|
|
```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<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.
|
|
|
|
```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.
|