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.
This commit is contained in:
136
backend/internal/domain/event_workflow.go
Normal file
136
backend/internal/domain/event_workflow.go
Normal file
@@ -0,0 +1,136 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
)
|
||||
|
||||
type EventState string
|
||||
|
||||
const (
|
||||
RegistrationOpen EventState = "RegistrationOpen"
|
||||
RegistrationClosed EventState = "RegistrationClosed"
|
||||
Balancing EventState = "Balancing"
|
||||
RostersDraft EventState = "RostersDraft"
|
||||
RostersConfirmed EventState = "RostersConfirmed"
|
||||
Live EventState = "Live"
|
||||
Completed EventState = "Completed"
|
||||
Cancelled EventState = "Cancelled"
|
||||
)
|
||||
|
||||
func (e *Event) Transition(from []EventState, to EventState, expectedVersion int) error {
|
||||
if e.Version != expectedVersion {
|
||||
return fmt.Errorf("%w: stale event version", ErrConflict)
|
||||
}
|
||||
if !slices.Contains(from, e.State) {
|
||||
return fmt.Errorf("%w: event is %s", ErrConflict, e.State)
|
||||
}
|
||||
e.State = to
|
||||
e.Version++
|
||||
return nil
|
||||
}
|
||||
|
||||
type RosterDraft struct {
|
||||
EventID string `json:"eventId"`
|
||||
Teams []Team `json:"teams"`
|
||||
Reserve []string `json:"reserve"`
|
||||
Version int `json:"version"`
|
||||
Confirmed bool `json:"confirmed"`
|
||||
}
|
||||
|
||||
func (r RosterDraft) Validate(requireCaptains bool) error {
|
||||
if r.EventID == "" || len(r.Teams) < 2 {
|
||||
return fmt.Errorf("%w: at least two teams are required", ErrInvalid)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, team := range r.Teams {
|
||||
if len(team.Slots) != 5 {
|
||||
return fmt.Errorf("%w: every team must contain five players", ErrInvalid)
|
||||
}
|
||||
counts := map[Role]int{}
|
||||
for _, slot := range team.Slots {
|
||||
if slot.PlayerID == "" || seen[slot.PlayerID] {
|
||||
return fmt.Errorf("%w: roster players must be unique", ErrInvalid)
|
||||
}
|
||||
seen[slot.PlayerID] = true
|
||||
counts[slot.Role]++
|
||||
}
|
||||
if counts[Tank] != 1 || counts[Damage] != 2 || counts[Support] != 2 {
|
||||
return fmt.Errorf("%w: every team must use 1/2/2", ErrInvalid)
|
||||
}
|
||||
if requireCaptains && !slices.ContainsFunc(team.Slots, func(slot Slot) bool { return slot.PlayerID == team.CaptainPlayerID }) {
|
||||
return fmt.Errorf("%w: every team needs a captain", ErrInvalid)
|
||||
}
|
||||
}
|
||||
for _, playerID := range r.Reserve {
|
||||
if playerID == "" || seen[playerID] {
|
||||
return fmt.Errorf("%w: reserve players must be unique", ErrInvalid)
|
||||
}
|
||||
seen[playerID] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RosterDraft) Swap(teamAID, playerAID, teamBID, playerBID string) error {
|
||||
if r.Confirmed {
|
||||
return fmt.Errorf("%w: rosters are locked", ErrConflict)
|
||||
}
|
||||
var a, b *Slot
|
||||
for teamIndex := range r.Teams {
|
||||
for slotIndex := range r.Teams[teamIndex].Slots {
|
||||
slot := &r.Teams[teamIndex].Slots[slotIndex]
|
||||
if r.Teams[teamIndex].ID == teamAID && slot.PlayerID == playerAID {
|
||||
a = slot
|
||||
}
|
||||
if r.Teams[teamIndex].ID == teamBID && slot.PlayerID == playerBID {
|
||||
b = slot
|
||||
}
|
||||
}
|
||||
}
|
||||
if a == nil || b == nil {
|
||||
return ErrNotFound
|
||||
}
|
||||
if a.Role != b.Role {
|
||||
return fmt.Errorf("%w: only equal roles can be swapped", ErrInvalid)
|
||||
}
|
||||
a.PlayerID, b.PlayerID = b.PlayerID, a.PlayerID
|
||||
a.Rating, b.Rating = b.Rating, a.Rating
|
||||
r.Version++
|
||||
return r.Validate(false)
|
||||
}
|
||||
|
||||
func (r *RosterDraft) Substitute(teamID, outgoingID, reserveID string, rating int) error {
|
||||
if r.Confirmed {
|
||||
return fmt.Errorf("%w: rosters are locked", ErrConflict)
|
||||
}
|
||||
reserveIndex := slices.Index(r.Reserve, reserveID)
|
||||
if reserveIndex < 0 {
|
||||
return fmt.Errorf("%w: player is not in reserve", ErrInvalid)
|
||||
}
|
||||
for teamIndex := range r.Teams {
|
||||
if r.Teams[teamIndex].ID != teamID {
|
||||
continue
|
||||
}
|
||||
for slotIndex := range r.Teams[teamIndex].Slots {
|
||||
slot := &r.Teams[teamIndex].Slots[slotIndex]
|
||||
if slot.PlayerID == outgoingID {
|
||||
slot.PlayerID, slot.Rating = reserveID, rating
|
||||
r.Reserve[reserveIndex] = outgoingID
|
||||
if r.Teams[teamIndex].CaptainPlayerID == outgoingID {
|
||||
r.Teams[teamIndex].CaptainPlayerID = ""
|
||||
}
|
||||
r.Version++
|
||||
return r.Validate(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
func (r *RosterDraft) EmergencySubstitute(teamID, outgoingID, reserveID string, rating int) error {
|
||||
confirmed := r.Confirmed
|
||||
r.Confirmed = false
|
||||
err := r.Substitute(teamID, outgoingID, reserveID, rating)
|
||||
r.Confirmed = confirmed
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user