package db import ( "context" "sort" "github.com/pkg/errors" "github.com/uptrace/bun" ) type Team struct { bun.BaseModel `bun:"table:teams,alias:t"` ID int `bun:"id,pk,autoincrement" json:"id"` Name string `bun:"name,unique,notnull" json:"name"` ShortName string `bun:"short_name,notnull,unique:short_names" json:"short_name"` AltShortName string `bun:"alt_short_name,notnull,unique:short_names" json:"alt_short_name"` Color string `bun:"color" json:"color,omitempty"` Seasons []Season `bun:"m2m:team_participations,join:Team=Season" json:"-"` Leagues []League `bun:"m2m:team_participations,join:Team=League" json:"-"` Players []Player `bun:"m2m:team_rosters,join:Team=Player" json:"-"` } func NewTeam(ctx context.Context, tx bun.Tx, name, shortName, altShortName, color string, audit *AuditMeta) (*Team, error) { team := &Team{ Name: name, ShortName: shortName, AltShortName: altShortName, Color: color, } err := Insert(tx, team). WithAudit(audit, nil).Exec(ctx) if err != nil { return nil, errors.Wrap(err, "db.Insert") } return team, nil } func ListTeams(ctx context.Context, tx bun.Tx, pageOpts *PageOpts) (*List[Team], error) { defaults := &PageOpts{ 1, 10, bun.OrderAsc, "name", } return GetList[Team](tx).GetPaged(ctx, pageOpts, defaults) } func GetTeam(ctx context.Context, tx bun.Tx, id int) (*Team, error) { if id == 0 { return nil, errors.New("id not provided") } return GetByID[Team](tx, id).Relation("Seasons").Relation("Leagues").Get(ctx) } func TeamShortNamesUnique(ctx context.Context, tx bun.Tx, shortName, altShortName string) (bool, error) { // Check if this combination of short_name and alt_short_name exists count, err := tx.NewSelect(). Model((*Team)(nil)). Where("short_name = ? AND alt_short_name = ?", shortName, altShortName). Count(ctx) if err != nil { return false, errors.Wrap(err, "tx.Select") } return count == 0, nil } func (t *Team) InSeason(seasonID int) bool { for _, season := range t.Seasons { if season.ID == seasonID { return true } } return false } // TeamSeasonInfo holds information about a team's participation in a specific season+league. type TeamSeasonInfo struct { Season *Season League *League Record *TeamRecord TotalTeams int Position int } // GetTeamSeasonParticipation returns all season+league combos the team participated in, // with computed records, positions, and total team counts. func GetTeamSeasonParticipation( ctx context.Context, tx bun.Tx, teamID int, ) ([]*TeamSeasonInfo, error) { if teamID == 0 { return nil, errors.New("teamID not provided") } // Get all participations for this team var participations []*TeamParticipation err := tx.NewSelect(). Model(&participations). Where("team_id = ?", teamID). Relation("Season", func(q *bun.SelectQuery) *bun.SelectQuery { return q.Relation("Leagues") }). Relation("League"). Scan(ctx) if err != nil { return nil, errors.Wrap(err, "tx.NewSelect participations") } var results []*TeamSeasonInfo for _, p := range participations { // Get all teams in this season+league for position calculation var teams []*Team err := tx.NewSelect(). Model(&teams). Join("INNER JOIN team_participations AS tp ON tp.team_id = t.id"). Where("tp.season_id = ? AND tp.league_id = ?", p.SeasonID, p.LeagueID). Scan(ctx) if err != nil { return nil, errors.Wrap(err, "tx.NewSelect teams") } // Get all fixtures for this season+league fixtures, err := GetAllocatedFixtures(ctx, tx, p.SeasonID, p.LeagueID) if err != nil { return nil, errors.Wrap(err, "GetAllocatedFixtures") } fixtureIDs := make([]int, len(fixtures)) for i, f := range fixtures { fixtureIDs[i] = f.ID } resultMap, err := GetFinalizedResultsForFixtures(ctx, tx, fixtureIDs) if err != nil { return nil, errors.Wrap(err, "GetFinalizedResultsForFixtures") } // Compute leaderboard to get position leaderboard := ComputeLeaderboard(teams, fixtures, resultMap) var position int var record *TeamRecord for _, entry := range leaderboard { if entry.Team.ID == teamID { position = entry.Position record = entry.Record break } } if record == nil { record = &TeamRecord{} } results = append(results, &TeamSeasonInfo{ Season: p.Season, League: p.League, Record: record, TotalTeams: len(teams), Position: position, }) } // Sort by season start date descending (newest first) sort.Slice(results, func(i, j int) bool { return results[i].Season.StartDate.After(results[j].Season.StartDate) }) return results, nil } // TeamAllTimePlayerStats holds aggregated all-time stats for a player on a team. type TeamAllTimePlayerStats struct { PlayerID int `bun:"player_id"` PlayerName string `bun:"player_name"` SeasonsPlayed int `bun:"seasons_played"` PeriodsPlayed int `bun:"total_periods_played"` Goals int `bun:"total_goals"` Assists int `bun:"total_assists"` Saves int `bun:"total_saves"` } // GetTeamAllTimePlayerStats returns aggregated all-time stats for all players // who have ever played for a given team across all seasons. func GetTeamAllTimePlayerStats( ctx context.Context, tx bun.Tx, teamID int, ) ([]*TeamAllTimePlayerStats, error) { if teamID == 0 { return nil, errors.New("teamID not provided") } var stats []*TeamAllTimePlayerStats err := tx.NewRaw(` SELECT frps.player_id AS player_id, COALESCE(p.name, frps.player_username) AS player_name, COUNT(DISTINCT s.id) AS seasons_played, COALESCE(SUM(frps.periods_played), 0) AS total_periods_played, COALESCE(SUM(frps.goals), 0) AS total_goals, COALESCE(SUM(frps.assists), 0) AS total_assists, COALESCE(SUM(frps.saves), 0) AS total_saves FROM fixture_result_player_stats frps JOIN fixture_results fr ON fr.id = frps.fixture_result_id JOIN fixtures f ON f.id = fr.fixture_id JOIN seasons s ON s.id = f.season_id LEFT JOIN players p ON p.id = frps.player_id WHERE fr.finalized = true AND frps.team_id = ? AND frps.period_num = 3 AND frps.player_id IS NOT NULL GROUP BY frps.player_id, COALESCE(p.name, frps.player_username) `, teamID).Scan(ctx, &stats) if err != nil { return nil, errors.Wrap(err, "tx.NewRaw") } return stats, nil }