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

@@ -24,8 +24,14 @@ type Store interface {
GetEvent(context.Context, string) (domain.Event, error)
UpdateEvent(context.Context, domain.Event) (domain.Event, error)
ListEvents(context.Context, time.Time) ([]domain.Event, error)
SaveEventWorkflow(context.Context, domain.Event, int) (domain.Event, error)
SaveRoster(context.Context, domain.RosterDraft, int) (domain.RosterDraft, error)
GetRoster(context.Context, string) (domain.RosterDraft, error)
StartScrim(context.Context, domain.Event, int, []domain.Series, *domain.Tournament) error
DeleteEvent(context.Context, string) error
UpsertRSVP(context.Context, domain.RSVP) (domain.RSVP, error)
ListRSVPs(context.Context, string) ([]domain.RSVP, error)
DeleteRSVP(context.Context, string, string) error
SaveTeams(context.Context, string, []domain.Team) error
ListTeams(context.Context, string) ([]domain.Team, error)
AssignCaptain(context.Context, string, string) (domain.Team, error)
@@ -67,7 +73,7 @@ func (s *Service) Authenticate(ctx context.Context, session string) (domain.Acco
return s.Store.AccountBySession(ctx, session)
}
func (s *Service) UpdateOwnProfile(ctx context.Context, actor domain.Account, current domain.Player, displayName string, ratings domain.Ratings, preferredRoles []domain.Role, preferredPlayerIDs []string) (domain.Player, error) {
func (s *Service) UpdateOwnProfile(ctx context.Context, actor domain.Account, current domain.Player, displayName string, ratings domain.Ratings, preferredRoles []domain.Role, preferredPlayerIDs, avoidedPlayerIDs []string) (domain.Player, error) {
if actor.ID != current.AccountID {
return domain.Player{}, domain.ErrForbidden
}
@@ -80,6 +86,7 @@ func (s *Service) UpdateOwnProfile(ctx context.Context, actor domain.Account, cu
current.Ratings = ratings
current.PreferredRoles = preferredRoles
current.PreferredPlayerIDs = preferredPlayerIDs
current.AvoidedPlayerIDs = avoidedPlayerIDs
current.UpdatedAt = s.Now()
if err := current.ValidatePreferences(); err != nil {
return domain.Player{}, err
@@ -97,7 +104,16 @@ func (s *Service) UpdateOwnProfile(ctx context.Context, actor domain.Account, cu
return domain.Player{}, fmt.Errorf("%w: preferred teammate does not exist", domain.ErrInvalid)
}
}
return s.Store.UpdatePlayer(ctx, current)
for _, playerID := range current.AvoidedPlayerIDs {
if !knownPlayers[playerID] {
return domain.Player{}, fmt.Errorf("%w: avoided teammate does not exist", domain.ErrInvalid)
}
}
out, err := s.Store.UpdatePlayer(ctx, current)
if err == nil {
s.Bus.Publish("players", out)
}
return out, err
}
func (s *Service) CreateEvent(ctx context.Context, actor domain.Account, event domain.Event) (domain.Event, error) {
@@ -108,6 +124,10 @@ func (s *Service) CreateEvent(ctx context.Context, actor domain.Account, event d
event.RegistrationDeadline = event.StartsAt
}
event.ID, event.CreatedBy, event.CreatedAt, event.UpdatedAt = NewID(), actor.ID, s.Now(), s.Now()
event.State, event.Version = domain.RegistrationOpen, 0
if event.RulesetID == "" {
event.RulesetID = "standard-control-hybrid-control"
}
if err := event.Validate(); err != nil {
return domain.Event{}, err
}
@@ -161,6 +181,9 @@ func (s *Service) SetRSVP(ctx context.Context, actor domain.Account, actorPlayer
if err != nil {
return domain.RSVP{}, err
}
if event.State != domain.RegistrationOpen {
return domain.RSVP{}, fmt.Errorf("%w: registration is closed", domain.ErrConflict)
}
if !actor.IsAdmin() && s.Now().After(event.RegistrationDeadline) {
return domain.RSVP{}, fmt.Errorf("%w: registration deadline has passed", domain.ErrConflict)
}
@@ -176,6 +199,25 @@ func (s *Service) SetRSVP(ctx context.Context, actor domain.Account, actorPlayer
return out, err
}
func (s *Service) RemoveParticipant(ctx context.Context, actor domain.Account, eventID, playerID string) error {
if !actor.IsAdmin() {
return domain.ErrForbidden
}
event, err := s.Store.GetEvent(ctx, eventID)
if err != nil {
return err
}
if event.State != domain.RegistrationOpen {
return fmt.Errorf("%w: registration is closed", domain.ErrConflict)
}
if err := s.Store.DeleteRSVP(ctx, eventID, playerID); err != nil {
return err
}
_ = s.Store.AppendAudit(ctx, actor.ID, "participant.removed", eventID, map[string]string{"playerId": playerID})
s.Bus.Publish("event:"+eventID, map[string]string{"removedPlayerId": playerID})
return nil
}
func (s *Service) CreateParticipant(ctx context.Context, actor domain.Account, eventID, displayName string, ratings domain.Ratings, status domain.RSVPStatus) (domain.Player, domain.RSVP, error) {
if !actor.IsAdmin() {
return domain.Player{}, domain.RSVP{}, domain.ErrForbidden
@@ -187,9 +229,13 @@ func (s *Service) CreateParticipant(ctx context.Context, actor domain.Account, e
if err := ratings.Validate(); err != nil {
return domain.Player{}, domain.RSVP{}, err
}
if _, err := s.Store.GetEvent(ctx, eventID); err != nil {
event, err := s.Store.GetEvent(ctx, eventID)
if err != nil {
return domain.Player{}, domain.RSVP{}, err
}
if event.State != domain.RegistrationOpen {
return domain.Player{}, domain.RSVP{}, fmt.Errorf("%w: registration is closed", domain.ErrConflict)
}
now := s.Now()
player := domain.Player{
ID: NewID(),
@@ -209,10 +255,11 @@ func (s *Service) CreateParticipant(ctx context.Context, actor domain.Account, e
if err := rsvp.Validate(); err != nil {
return domain.Player{}, domain.RSVP{}, err
}
player, rsvp, err := s.Store.CreateParticipant(ctx, player, rsvp)
player, rsvp, err = s.Store.CreateParticipant(ctx, player, rsvp)
if err == nil {
_ = s.Store.AppendAudit(ctx, actor.ID, "participant.created", eventID, map[string]any{"player": player, "rsvp": rsvp})
s.Bus.Publish("event:"+eventID, rsvp)
s.Bus.Publish("players", player)
}
return player, rsvp, err
}