Files
oslstats/internal/db/season.go
2026-03-09 13:01:28 +11:00

220 lines
6.4 KiB
Go

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)
}