Add event management features to API, including event cancellation, deletion, registration closure, and workflow balance generation. Introduce new roster management endpoints and enhance participant handling with avoided player preferences. Update database schema and service logic to support these changes.
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / compose (push) Has been cancelled

This commit is contained in:
2026-07-19 02:10:02 +03:00
parent 59c0bdb345
commit 4b889fb8a0
30 changed files with 2766 additions and 149 deletions

View File

@@ -0,0 +1,186 @@
package domain
import (
"fmt"
"slices"
"time"
)
type SeriesPhase string
const (
CoinTossPending SeriesPhase = "CoinTossPending"
MapBanPhase SeriesPhase = "MapBan"
MapPickPhase SeriesPhase = "MapPick"
HeroBanPhase SeriesPhase = "HeroBan"
PlayingPhase SeriesPhase = "Playing"
SeriesComplete SeriesPhase = "Completed"
)
type SeriesAction struct {
Kind string `json:"kind"`
Summary string `json:"summary"`
TeamID string `json:"teamId,omitempty"`
ActorAccountID string `json:"actorAccountId"`
At time.Time `json:"at"`
}
type SeriesMap struct {
Number int `json:"number"`
Name string `json:"name"`
HeroBans []DraftAction `json:"heroBans"`
Result *MapResult `json:"result,omitempty"`
}
func NewSeries(id, eventID, tournamentID string, teams [2]string, rules Ruleset) (*Series, error) {
if id == "" || eventID == "" || teams[0] == "" || teams[1] == "" || teams[0] == teams[1] {
return nil, fmt.Errorf("%w: invalid series", ErrInvalid)
}
if err := rules.Validate(); err != nil {
return nil, err
}
return &Series{
ID: id, EventID: eventID, TournamentID: tournamentID,
TeamAID: teams[0], TeamBID: teams[1], BestOf: rules.BestOf,
RulesetID: rules.ID, Phase: CoinTossPending,
SeriesBans: map[string][]string{},
}, nil
}
func (s *Series) Toss(seed, actor string, now time.Time, rules Ruleset) error {
if s.Phase != CoinTossPending {
return fmt.Errorf("%w: coin toss is unavailable", ErrConflict)
}
toss, err := TossCoin(s.TeamAID, s.TeamBID, seed, now)
if err != nil {
return err
}
s.CoinToss = &toss
first := toss.WinnerTeamID
if rules.InitialMapBanner == 1 {
first = s.otherTeam(first)
}
draft, err := NewMapDraft(s.availablePool(rules, 0), first, [2]string{s.TeamAID, s.TeamBID})
if err != nil {
return err
}
s.MapDraft, s.Phase = draft, MapBanPhase
s.Version++
s.Audit = append(s.Audit, SeriesAction{Kind: "coin_toss", Summary: "Coin toss won by " + toss.WinnerTeamID, TeamID: toss.WinnerTeamID, ActorAccountID: actor, At: now})
return nil
}
func (s *Series) BanMap(teamID, name, actor string, now time.Time, rules Ruleset) error {
if s.Phase != MapBanPhase || s.MapDraft == nil {
return fmt.Errorf("%w: map ban is unavailable", ErrConflict)
}
if err := s.MapDraft.Ban(teamID, name, actor, now); err != nil {
return err
}
s.Version++
s.Audit = append(s.Audit, SeriesAction{Kind: "map_ban", Summary: name + " banned", TeamID: teamID, ActorAccountID: actor, At: now})
selected, complete := s.MapDraft.Selected()
if !complete {
return nil
}
return s.startHeroDraft(selected, actor, now, rules)
}
func (s *Series) PickMap(teamID, name, actor string, now time.Time, rules Ruleset) error {
if s.Phase != MapPickPhase {
return fmt.Errorf("%w: map pick is unavailable", ErrConflict)
}
if teamID != s.NextMapPickerID {
return fmt.Errorf("%w: only the previous map loser can pick", ErrConflict)
}
if !slices.Contains(s.AvailableMaps, name) {
return fmt.Errorf("%w: map unavailable", ErrInvalid)
}
s.Version++
s.Audit = append(s.Audit, SeriesAction{Kind: "map_selected", Summary: name + " picked", TeamID: teamID, ActorAccountID: actor, At: now})
return s.startHeroDraft(name, actor, now, rules)
}
func (s *Series) startHeroDraft(selected, actor string, now time.Time, rules Ruleset) error {
s.CurrentMap = selected
first := s.otherTeam(s.CoinToss.WinnerTeamID)
if rules.InitialHeroBanner == 0 {
first = s.CoinToss.WinnerTeamID
}
draft, err := NewHeroDraft(rules.Heroes, [2]string{s.TeamAID, s.TeamBID}, first, rules.HeroBansPerTeam, s.SeriesBans)
if err != nil {
return err
}
s.HeroDraft, s.Phase = draft, HeroBanPhase
s.NextMapPickerID, s.AvailableMaps = "", nil
s.Maps = append(s.Maps, SeriesMap{Number: len(s.Maps) + 1, Name: selected})
if len(s.Audit) == 0 || s.Audit[len(s.Audit)-1].Kind != "map_selected" {
s.Audit = append(s.Audit, SeriesAction{Kind: "map_selected", Summary: selected + " selected", ActorAccountID: actor, At: now})
}
return nil
}
func (s *Series) BanHero(teamID, hero, actor string, now time.Time) error {
if s.Phase != HeroBanPhase || s.HeroDraft == nil {
return fmt.Errorf("%w: hero ban is unavailable", ErrConflict)
}
if err := s.HeroDraft.Ban(teamID, hero, actor, now); err != nil {
return err
}
s.Version++
s.Maps[len(s.Maps)-1].HeroBans = slices.Clone(s.HeroDraft.CurrentBans)
s.Audit = append(s.Audit, SeriesAction{Kind: "hero_ban", Summary: hero + " banned", TeamID: teamID, ActorAccountID: actor, At: now})
if s.HeroDraft.Complete() {
s.SeriesBans = s.HeroDraft.SeriesBans
s.Phase = PlayingPhase
}
return nil
}
func (s *Series) RecordCurrentMap(outcome MapOutcome, actor string, now time.Time, rules Ruleset) error {
if s.Phase != PlayingPhase || s.CurrentMap == "" {
return fmt.Errorf("%w: map result is unavailable", ErrConflict)
}
result := MapResult{MapName: s.CurrentMap, Outcome: outcome, ActorAccountID: actor, RecordedAt: now}
if err := s.RecordResult(result); err != nil {
return err
}
s.PlayedMaps = append(s.PlayedMaps, s.CurrentMap)
s.Maps[len(s.Maps)-1].Result = &result
s.Audit = append(s.Audit, SeriesAction{Kind: "result", Summary: s.CurrentMap + ": " + string(outcome), ActorAccountID: actor, At: now})
if s.WinnerTeamID != "" {
s.Phase = SeriesComplete
return nil
}
poolIndex := len(s.Maps)
if poolIndex >= len(rules.MapPools) {
poolIndex = len(rules.MapPools) - 1
}
switch outcome {
case TeamAWin:
s.NextMapPickerID = s.TeamBID
case TeamBWin:
s.NextMapPickerID = s.TeamAID
default:
// A draw has no loser, so the initial coin-toss loser receives the pick.
s.NextMapPickerID = s.otherTeam(s.CoinToss.WinnerTeamID)
}
s.AvailableMaps = s.availablePool(rules, poolIndex)
if len(s.AvailableMaps) == 0 {
return fmt.Errorf("%w: no unplayed maps remain in pool", ErrInvalid)
}
s.MapDraft, s.HeroDraft, s.CurrentMap, s.Phase = nil, nil, "", MapPickPhase
return nil
}
func (s Series) availablePool(rules Ruleset, index int) []string {
pool := slices.Clone(rules.MapPools[index])
return slices.DeleteFunc(pool, func(name string) bool { return slices.Contains(s.PlayedMaps, name) })
}
func (s Series) otherTeam(teamID string) string {
if teamID == s.TeamAID {
return s.TeamBID
}
return s.TeamAID
}