package db import ( "context" "strings" "time" "github.com/pkg/errors" "github.com/uptrace/bun" ) // SeasonStatus represents the current status of a season type SeasonStatus string const ( // StatusUpcoming means the season has not started yet StatusUpcoming SeasonStatus = "upcoming" // StatusInProgress means the regular season is active StatusInProgress SeasonStatus = "in_progress" // StatusFinalsSoon means regular season ended, finals upcoming StatusFinalsSoon SeasonStatus = "finals_soon" // StatusFinals means finals are in progress StatusFinals SeasonStatus = "finals" // StatusCompleted means the season has finished StatusCompleted SeasonStatus = "completed" ) type SeasonType string func (s SeasonType) String() string { return string(s) } const ( SeasonTypeRegular SeasonType = "regular" SeasonTypeDraft SeasonType = "draft" ) type Season struct { bun.BaseModel `bun:"table:seasons,alias:s"` ID int `bun:"id,pk,autoincrement" json:"id"` Name string `bun:"name,unique,notnull" json:"name"` ShortName string `bun:"short_name,unique,notnull" json:"short_name"` StartDate time.Time `bun:"start_date,notnull" json:"start_date"` EndDate bun.NullTime `bun:"end_date" json:"end_date"` FinalsStartDate bun.NullTime `bun:"finals_start_date" json:"finals_start_date"` FinalsEndDate bun.NullTime `bun:"finals_end_date" json:"finals_end_date"` SlapVersion string `bun:"slap_version,notnull,default:'rebound'" json:"slap_version"` Type string `bun:"type,notnull" json:"type"` Leagues []League `bun:"m2m:season_leagues,join:Season=League" json:"-"` Teams []Team `bun:"m2m:team_participations,join:Season=Team" json:"-"` } // NewSeason creats a new season func NewSeason(ctx context.Context, tx bun.Tx, name, version, shortname, type_ string, start time.Time, audit *AuditMeta, ) (*Season, error) { season := &Season{ Name: name, ShortName: strings.ToUpper(shortname), StartDate: start.Truncate(time.Hour * 24), SlapVersion: version, Type: type_, } err := Insert(tx, season). WithAudit(audit, nil).Exec(ctx) if err != nil { return nil, errors.WithMessage(err, "db.Insert") } if season.Type == SeasonTypeDraft.String() { err = NewSeasonLeague(ctx, tx, season.ShortName, "Draft", audit) if err != nil { return nil, errors.Wrap(err, "NewSeasonLeague") } } return season, nil } func ListSeasons(ctx context.Context, tx bun.Tx, pageOpts *PageOpts) (*List[Season], error) { defaults := &PageOpts{ 1, 10, bun.OrderDesc, "start_date", } return GetList[Season](tx).Relation("Leagues").GetPaged(ctx, pageOpts, defaults) } func GetSeason(ctx context.Context, tx bun.Tx, shortname string) (*Season, error) { if shortname == "" { return nil, errors.New("short_name not provided") } return GetByField[Season](tx, "short_name", shortname).Relation("Leagues").Relation("Teams").Get(ctx) } // Update updates the season struct. It does not insert to the database func (s *Season) Update(ctx context.Context, tx bun.Tx, version string, start, end, finalsStart, finalsEnd time.Time, audit *AuditMeta, ) error { s.SlapVersion = version s.StartDate = start.Truncate(time.Hour * 24) if !end.IsZero() { s.EndDate.Time = end.Truncate(time.Hour * 24) } if !finalsStart.IsZero() { s.FinalsStartDate.Time = finalsStart.Truncate(time.Hour * 24) } if !finalsEnd.IsZero() { s.FinalsEndDate.Time = finalsEnd.Truncate(time.Hour * 24) } return Update(tx, s).WherePK(). Column("slap_version", "start_date", "end_date", "finals_start_date", "finals_end_date"). WithAudit(audit, nil).Exec(ctx) } func (s *Season) MapTeamsToLeagues(ctx context.Context, tx bun.Tx) ([]LeagueWithTeams, error) { // For each league, get the teams leaguesWithTeams := make([]LeagueWithTeams, len(s.Leagues)) for i, league := range s.Leagues { 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 = ?", s.ID, league.ID). Order("t.name ASC"). Scan(ctx) if err != nil { return nil, errors.Wrap(err, "tx.NewSelect") } leaguesWithTeams[i] = LeagueWithTeams{ League: &league, Teams: teams, } } return leaguesWithTeams, nil } type LeagueWithTeams struct { League *League Teams []*Team } // GetStatus returns the current status of the season based on dates. // Dates are treated as inclusive days: // - StartDate: season is "in progress" from the start of this day // - EndDate: season is "in progress" through the end of this day // - FinalsStartDate: finals are active from the start of this day // - FinalsEndDate: finals are active through the end of this day func (s *Season) GetStatus() SeasonStatus { now := time.Now() if now.Before(s.StartDate) { return StatusUpcoming } // dayPassed returns true if the entire calendar day of t has passed. // e.g., if t is March 8, this returns true starting March 9 00:00:00. dayPassed := func(t time.Time) bool { return now.After(t.Truncate(time.Hour*24).AddDate(0, 0, 1)) } // dayStarted returns true if the calendar day of t has started. // e.g., if t is March 8, this returns true starting March 8 00:00:00. dayStarted := func(t time.Time) bool { return !now.Before(t.Truncate(time.Hour * 24)) } if !s.FinalsStartDate.IsZero() { if !s.FinalsEndDate.IsZero() && dayPassed(s.FinalsEndDate.Time) { return StatusCompleted } if dayStarted(s.FinalsStartDate.Time) { return StatusFinals } if !s.EndDate.IsZero() && dayPassed(s.EndDate.Time) { return StatusFinalsSoon } return StatusInProgress } if !s.EndDate.IsZero() && dayPassed(s.EndDate.Time) { return StatusCompleted } return StatusInProgress } // GetDefaultTab returns the default tab to show based on the season status func (s *Season) GetDefaultTab() string { switch s.GetStatus() { case StatusInProgress: return "table" case StatusUpcoming: return "teams" default: return "finals" } } func (s *Season) HasLeague(league *League) bool { for _, league_ := range s.Leagues { if league_.ID == league.ID { return true } } return false } func (s *Season) GetLeague(leagueShortName string) (*League, error) { for _, league := range s.Leagues { if league.ShortName == leagueShortName { return &league, nil } } return nil, BadRequestNotAssociated("season", "league", "id", "short_name", s.ID, leagueShortName) }