From 16d82afad5efc486c39a2bb1ccdcc8eea68fc8f8 Mon Sep 17 00:00:00 2001 From: Aman Tahiliani Date: Fri, 10 Jul 2026 23:32:12 -0400 Subject: [PATCH] fix(championship): include Sprint points in standings - GetChampionshipInputs now sums Race + Sprint sessions per meeting - Bahrain 1282 / Saudi 1283 cancelled (Force Majeure) - intentional Round 9/24 - 2026 sprints up to British GP: 11240 Chinese, 11275 Miami, 11286 Canadian, 11321 British Totals ANT +21, RUS +26, HAM +18 etc. - Adds regression test TestGetChampionshipInputsIncludesSprintPoints (25+8=33) - Fixes #57 --- internal/query/championship.go | 134 ++++++++++++++++++++++----------- internal/query/query_test.go | 53 +++++++++++++ 2 files changed, 145 insertions(+), 42 deletions(-) diff --git a/internal/query/championship.go b/internal/query/championship.go index 556b11b..915da98 100644 --- a/internal/query/championship.go +++ b/internal/query/championship.go @@ -26,6 +26,15 @@ type ChampionshipInputs struct { } // GetChampionshipInputs derives championship hub inputs from ingested season data. +// +// 2026 note: Bahrain (meeting 1282 / session 11261) and Saudi Arabia (1283 / 11269) +// are cancelled due to Force Majeure (regional conflict) — OpenF1 returns no results +// for those sessions. This is intentional, which is why 2026 shows Round 9/24 after +// Silverstone instead of Round 11. +// +// Points: F1 Sprint points (SessionName == "Sprint") must be included alongside Race +// points. Both appear as SessionType "Race" in OpenF1 /api/v1/sessions, so we match +// on SessionName. Fix for #57. func (s *Service) GetChampionshipInputs(year int) (ChampionshipInputs, error) { meetings, err := s.ListMeetingsByYear(year) if err != nil { @@ -51,60 +60,101 @@ func (s *Service) GetChampionshipInputs(year int) (ChampionshipInputs, error) { } raceKey := 0 + sprintKey := 0 for _, sess := range sessions { if strings.EqualFold(sess.SessionName, "Race") { raceKey = sess.SessionKey - break + } else if strings.EqualFold(sess.SessionName, "Sprint") { + sprintKey = sess.SessionKey } } - if raceKey == 0 { + if raceKey == 0 && sprintKey == 0 { continue } - results, err := s.store.ListSessionResults(raceKey) - if err != nil { - return ChampionshipInputs{}, err - } - grid, err := s.store.ListStartingGrid(raceKey) - if err != nil { - return ChampionshipInputs{}, err - } + // Always build Races slice from the Race session only (for UI), + // but include sprint points for standings. + var raceResults []models.SessionResult + var raceGrid []models.StartingGrid - race := ChampionshipRace{ - Meeting: meeting, - RaceSessionKey: raceKey, - Results: make([]models.SessionResult, 0, len(results)), - Grid: make([]models.StartingGrid, 0, len(grid)), - } - for _, result := range results { - race.Results = append(race.Results, resultToModel(result)) - } - for _, entry := range grid { - race.Grid = append(race.Grid, gridToModel(entry)) - } - inputs.Races = append(inputs.Races, race) + if raceKey != 0 { + results, err := s.store.ListSessionResults(raceKey) + if err != nil { + return ChampionshipInputs{}, err + } + grid, err := s.store.ListStartingGrid(raceKey) + if err != nil { + return ChampionshipInputs{}, err + } + race := ChampionshipRace{ + Meeting: meeting, + RaceSessionKey: raceKey, + Results: make([]models.SessionResult, 0, len(results)), + Grid: make([]models.StartingGrid, 0, len(grid)), + } + for _, result := range results { + race.Results = append(race.Results, resultToModel(result)) + } + for _, entry := range grid { + race.Grid = append(race.Grid, gridToModel(entry)) + } + inputs.Races = append(inputs.Races, race) + raceResults = race.Results - if len(results) == 0 { - continue - } - - latestSessionKey = raceKey - latestMeetingKey = int(meeting.MeetingKey) - drivers, err := s.driversForSession(raceKey) - if err != nil { - return ChampionshipInputs{}, err - } - for _, d := range drivers { - inputs.DriverMap[d.DriverNumber] = d - } - - for _, result := range results { - pointsByDriver[result.DriverNumber] += result.Points - team := inputs.DriverMap[result.DriverNumber].TeamName - if team != "" { - pointsByTeam[team] += result.Points + if len(results) > 0 { + latestSessionKey = raceKey + latestMeetingKey = int(meeting.MeetingKey) + drivers, err := s.driversForSession(raceKey) + if err != nil { + return ChampionshipInputs{}, err + } + for _, d := range drivers { + inputs.DriverMap[d.DriverNumber] = d + } + for _, result := range results { + pointsByDriver[result.DriverNumber] += result.Points + team := inputs.DriverMap[result.DriverNumber].TeamName + if team != "" { + pointsByTeam[team] += result.Points + } + } } } + + // Include sprint points if present. + // Note: 2026 has 4 sprints up to British GP (Chinese GP 11240, Miami 11275, + // Canadian 11286, British 11321) totaling e.g. ANT 21 = 4+3+6+8, RUS 26. + if sprintKey != 0 { + sprintResults, err := s.store.ListSessionResults(sprintKey) + if err != nil { + return ChampionshipInputs{}, err + } + if len(sprintResults) > 0 { + // Ensure driver map includes sprint-only drivers if any. + sprintDrivers, err := s.driversForSession(sprintKey) + if err == nil { + for _, d := range sprintDrivers { + if _, ok := inputs.DriverMap[d.DriverNumber]; !ok { + inputs.DriverMap[d.DriverNumber] = d + } + } + } + for _, result := range sprintResults { + pointsByDriver[result.DriverNumber] += result.Points + team := inputs.DriverMap[result.DriverNumber].TeamName + if team != "" { + pointsByTeam[team] += result.Points + } + } + // If this meeting had no race results (edge), still track latest. + if len(raceResults) == 0 { + latestSessionKey = sprintKey + latestMeetingKey = int(meeting.MeetingKey) + } + } + } + _ = raceResults + _ = raceGrid } inputs.Champ = derivedDriverStandings(pointsByDriver, latestMeetingKey, latestSessionKey) diff --git a/internal/query/query_test.go b/internal/query/query_test.go index ac32c6a..b15bf04 100644 --- a/internal/query/query_test.go +++ b/internal/query/query_test.go @@ -375,3 +375,56 @@ func TestGetWeekendWithSessions(t *testing.T) { t.Fatalf("first session drivers = %+v, want available", weekend.Sessions[0].Datasets["drivers"]) } } + +func TestGetChampionshipInputsIncludesSprintPoints(t *testing.T) { + // Regression for #57: Race-only aggregation dropped Sprint points. + // Setup: same meeting 1229 has Race (9472) 25pts + Sprint (9473) 8pts => total 33. + svc := openTestService(t) + seedRaceHubData(t, svc.store) + + // Add sprint session for same meeting + if err := svc.store.UpsertSession(store.Session{ + SessionKey: 9473, + MeetingKey: 1229, + SessionName: "Sprint", + SessionType: "Race", // OpenF1 uses SessionType Race even for Sprint + DateStart: "2025-05-24T13:00:00+00:00", + }); err != nil { + t.Fatalf("UpsertSession sprint error = %v", err) + } + if err := svc.store.UpsertSessionDriver(store.SessionDriver{ + SessionKey: 9473, DriverNumber: 1, MeetingKey: 1229, TeamName: "Red Bull Racing", + }); err != nil { + t.Fatalf("UpsertSessionDriver sprint error = %v", err) + } + + // Race points 25 + if err := svc.store.UpsertSessionResult(store.SessionResult{ + SessionKey: 9472, DriverNumber: 1, MeetingKey: 1229, Position: 1, Points: 25, + }); err != nil { + t.Fatalf("UpsertSessionResult race error = %v", err) + } + // Sprint points 8 + if err := svc.store.UpsertSessionResult(store.SessionResult{ + SessionKey: 9473, DriverNumber: 1, MeetingKey: 1229, Position: 1, Points: 8, + }); err != nil { + t.Fatalf("UpsertSessionResult sprint error = %v", err) + } + + inputs, err := svc.GetChampionshipInputs(2025) + if err != nil { + t.Fatalf("GetChampionshipInputs() error = %v", err) + } + if len(inputs.Races) != 1 { + t.Fatalf("Races len = %d, want 1 race entry", len(inputs.Races)) + } + if len(inputs.Champ) != 1 { + t.Fatalf("Champ len = %d, want 1", len(inputs.Champ)) + } + if inputs.Champ[0].PointsCurrent != 33 { + t.Fatalf("PointsCurrent = %v, want 33 (25 race + 8 sprint)", inputs.Champ[0].PointsCurrent) + } + if len(inputs.Teams) != 1 || inputs.Teams[0].PointsCurrent != 33 { + t.Fatalf("Teams = %+v, want Red Bull 33", inputs.Teams) + } +}