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
}

View File

@@ -0,0 +1,497 @@
package application
import (
"context"
"fmt"
"mixmaker/backend/internal/domain"
)
type BalanceWorkflow struct {
Event domain.Event `json:"event"`
Candidates []domain.BalanceCandidate `json:"candidates"`
}
type ScrimStart struct {
Event domain.Event `json:"event"`
Series *domain.Series `json:"series,omitempty"`
Tournament *domain.Tournament `json:"tournament,omitempty"`
}
func (s *Service) CancelEvent(ctx context.Context, actor domain.Account, eventID string, expectedVersion int) (domain.Event, error) {
if !actor.IsAdmin() {
return domain.Event{}, domain.ErrForbidden
}
event, err := s.Store.GetEvent(ctx, eventID)
if err != nil {
return domain.Event{}, err
}
if event.State == domain.Completed || event.State == domain.Cancelled {
return domain.Event{}, fmt.Errorf("%w: event cannot be cancelled", domain.ErrConflict)
}
oldVersion := event.Version
event.State, event.Version, event.UpdatedAt = domain.Cancelled, event.Version+1, s.Now()
if oldVersion != expectedVersion {
return domain.Event{}, fmt.Errorf("%w: stale event version", domain.ErrConflict)
}
event, err = s.Store.SaveEventWorkflow(ctx, event, oldVersion)
if err == nil {
_ = s.Store.AppendAudit(ctx, actor.ID, "event.cancelled", eventID, event)
s.Bus.Publish("event:"+eventID, event)
s.Bus.Publish("events", event)
}
return event, err
}
func (s *Service) DeleteEvent(ctx context.Context, actor domain.Account, eventID string) error {
if !actor.IsAdmin() {
return domain.ErrForbidden
}
if err := s.Store.DeleteEvent(ctx, eventID); err != nil {
return err
}
tombstone := map[string]any{"eventId": eventID, "deleted": true}
s.Bus.Publish("event:"+eventID, tombstone)
s.Bus.Publish("events", tombstone)
return nil
}
func (s *Service) CloseRegistration(ctx context.Context, actor domain.Account, eventID, rulesetID string, expectedVersion int) (domain.Event, error) {
if !actor.IsAdmin() {
return domain.Event{}, domain.ErrForbidden
}
event, err := s.Store.GetEvent(ctx, eventID)
if err != nil {
return domain.Event{}, err
}
if _, err = s.Store.GetRuleset(ctx, rulesetID); err != nil {
return domain.Event{}, err
}
oldVersion := event.Version
if err = event.Transition([]domain.EventState{domain.RegistrationOpen}, domain.RegistrationClosed, expectedVersion); err != nil {
return domain.Event{}, err
}
event.RulesetID, event.UpdatedAt = rulesetID, s.Now()
out, err := s.Store.SaveEventWorkflow(ctx, event, oldVersion)
if err == nil {
_ = s.Store.AppendAudit(ctx, actor.ID, "event.registration_closed", eventID, out)
s.Bus.Publish("event:"+eventID, out)
}
return out, err
}
func (s *Service) GenerateBalance(ctx context.Context, actor domain.Account, eventID string, expectedVersion int) (BalanceWorkflow, error) {
if !actor.IsAdmin() {
return BalanceWorkflow{}, domain.ErrForbidden
}
event, err := s.Store.GetEvent(ctx, eventID)
if err != nil {
return BalanceWorkflow{}, err
}
oldVersion := event.Version
if err = event.Transition([]domain.EventState{domain.RegistrationClosed, domain.Balancing}, domain.Balancing, expectedVersion); err != nil {
return BalanceWorkflow{}, err
}
event.UpdatedAt = s.Now()
event, err = s.Store.SaveEventWorkflow(ctx, event, oldVersion)
if err != nil {
return BalanceWorkflow{}, err
}
candidates, err := s.Balance(ctx, actor, eventID)
if err == nil {
_ = s.Store.AppendAudit(ctx, actor.ID, "balance.generated", eventID, map[string]int{"count": len(candidates)})
s.Bus.Publish("event:"+eventID, event)
}
return BalanceWorkflow{Event: event, Candidates: candidates}, err
}
func (s *Service) SelectWorkflowBalance(ctx context.Context, actor domain.Account, eventID string, candidate domain.BalanceCandidate, expectedVersion int) (domain.RosterDraft, error) {
if !actor.IsAdmin() {
return domain.RosterDraft{}, domain.ErrForbidden
}
event, err := s.Store.GetEvent(ctx, eventID)
if err != nil {
return domain.RosterDraft{}, err
}
oldVersion := event.Version
if err = event.Transition([]domain.EventState{domain.Balancing}, domain.RostersDraft, expectedVersion); err != nil {
return domain.RosterDraft{}, err
}
roster := domain.RosterDraft{EventID: eventID, Teams: candidate.Teams, Reserve: candidate.Reserve, Version: 0}
if err = roster.Validate(false); err != nil {
return domain.RosterDraft{}, err
}
if roster, err = s.Store.SaveRoster(ctx, roster, -1); err != nil {
return domain.RosterDraft{}, err
}
if err = s.Store.SaveTeams(ctx, eventID, roster.Teams); err != nil {
return domain.RosterDraft{}, err
}
event.UpdatedAt = s.Now()
if _, err = s.Store.SaveEventWorkflow(ctx, event, oldVersion); err != nil {
return domain.RosterDraft{}, err
}
_ = s.Store.AppendAudit(ctx, actor.ID, "roster.created", eventID, roster)
s.Bus.Publish("event:"+eventID, roster)
return roster, nil
}
func (s *Service) SwapRoster(ctx context.Context, actor domain.Account, eventID, teamA, playerA, teamB, playerB string, expectedVersion int) (domain.RosterDraft, error) {
if !actor.IsAdmin() {
return domain.RosterDraft{}, domain.ErrForbidden
}
roster, err := s.Store.GetRoster(ctx, eventID)
if err != nil {
return roster, err
}
if roster.Version != expectedVersion {
return roster, fmt.Errorf("%w: stale roster version", domain.ErrConflict)
}
if err = roster.Swap(teamA, playerA, teamB, playerB); err != nil {
return roster, err
}
roster, err = s.Store.SaveRoster(ctx, roster, expectedVersion)
if err == nil {
_ = s.Store.SaveTeams(ctx, eventID, roster.Teams)
_ = s.Store.AppendAudit(ctx, actor.ID, "roster.swapped", eventID, map[string]string{"playerAId": playerA, "playerBId": playerB})
s.Bus.Publish("event:"+eventID, roster)
}
return roster, err
}
func (s *Service) SubstituteRoster(ctx context.Context, actor domain.Account, eventID, teamID, outgoingID, reserveID string, expectedVersion int, emergency bool) (domain.RosterDraft, error) {
if !actor.IsAdmin() {
return domain.RosterDraft{}, domain.ErrForbidden
}
event, err := s.Store.GetEvent(ctx, eventID)
if err != nil {
return domain.RosterDraft{}, err
}
if emergency != (event.State == domain.Live) {
return domain.RosterDraft{}, fmt.Errorf("%w: substitution is unavailable in this state", domain.ErrConflict)
}
roster, err := s.Store.GetRoster(ctx, eventID)
if err != nil {
return roster, err
}
if roster.Version != expectedVersion {
return roster, fmt.Errorf("%w: stale roster version", domain.ErrConflict)
}
players, err := s.Store.ListPlayers(ctx)
if err != nil {
return roster, err
}
rating := 0
role := domain.Role("")
for _, team := range roster.Teams {
for _, slot := range team.Slots {
if team.ID == teamID && slot.PlayerID == outgoingID {
role = slot.Role
}
}
}
for _, player := range players {
if player.ID == reserveID {
rating = ratingForRole(player, role)
}
}
if rating == 0 {
return roster, domain.ErrNotFound
}
if emergency {
err = roster.EmergencySubstitute(teamID, outgoingID, reserveID, rating)
} else {
err = roster.Substitute(teamID, outgoingID, reserveID, rating)
}
if err != nil {
return roster, err
}
roster, err = s.Store.SaveRoster(ctx, roster, expectedVersion)
if err == nil {
_ = s.Store.SaveTeams(ctx, eventID, roster.Teams)
action := "roster.substituted"
if emergency {
action = "roster.emergency_substituted"
}
_ = s.Store.AppendAudit(ctx, actor.ID, action, eventID, map[string]string{"outgoingPlayerId": outgoingID, "incomingPlayerId": reserveID})
s.Bus.Publish("event:"+eventID, roster)
}
return roster, err
}
func (s *Service) SetRosterCaptain(ctx context.Context, actor domain.Account, eventID, teamID, playerID string, expectedVersion int) (domain.RosterDraft, error) {
if !actor.IsAdmin() {
return domain.RosterDraft{}, domain.ErrForbidden
}
roster, err := s.Store.GetRoster(ctx, eventID)
if err != nil {
return roster, err
}
if roster.Confirmed || roster.Version != expectedVersion {
return roster, domain.ErrConflict
}
found := false
for i := range roster.Teams {
if roster.Teams[i].ID == teamID {
if err = roster.Teams[i].AssignCaptain(playerID); err != nil {
return roster, err
}
found = true
}
}
if !found {
return roster, domain.ErrNotFound
}
roster.Version++
roster, err = s.Store.SaveRoster(ctx, roster, expectedVersion)
if err == nil {
_ = s.Store.SaveTeams(ctx, eventID, roster.Teams)
_ = s.Store.AppendAudit(ctx, actor.ID, "roster.captain_assigned", eventID, map[string]string{"teamId": teamID, "playerId": playerID})
s.Bus.Publish("event:"+eventID, roster)
}
return roster, err
}
func (s *Service) ConfirmRosters(ctx context.Context, actor domain.Account, eventID string, expectedEventVersion, expectedRosterVersion int) (domain.Event, error) {
if !actor.IsAdmin() {
return domain.Event{}, domain.ErrForbidden
}
event, err := s.Store.GetEvent(ctx, eventID)
if err != nil {
return event, err
}
roster, err := s.Store.GetRoster(ctx, eventID)
if err != nil {
return event, err
}
if roster.Version != expectedRosterVersion {
return event, domain.ErrConflict
}
if err = roster.Validate(true); err != nil {
return event, err
}
oldEventVersion := event.Version
if err = event.Transition([]domain.EventState{domain.RostersDraft}, domain.RostersConfirmed, expectedEventVersion); err != nil {
return event, err
}
roster.Confirmed, roster.Version = true, roster.Version+1
if _, err = s.Store.SaveRoster(ctx, roster, expectedRosterVersion); err != nil {
return event, err
}
event.UpdatedAt = s.Now()
event, err = s.Store.SaveEventWorkflow(ctx, event, oldEventVersion)
if err == nil {
_ = s.Store.AppendAudit(ctx, actor.ID, "roster.confirmed", eventID, roster)
s.Bus.Publish("event:"+eventID, event)
}
return event, err
}
func (s *Service) StartScrim(ctx context.Context, actor domain.Account, eventID string, expectedVersion int) (ScrimStart, error) {
if !actor.IsAdmin() {
return ScrimStart{}, domain.ErrForbidden
}
event, err := s.Store.GetEvent(ctx, eventID)
if err != nil {
return ScrimStart{}, err
}
roster, err := s.Store.GetRoster(ctx, eventID)
if err != nil {
return ScrimStart{}, err
}
if !roster.Confirmed {
return ScrimStart{}, fmt.Errorf("%w: rosters are not confirmed", domain.ErrConflict)
}
rules, err := s.Store.GetRuleset(ctx, event.RulesetID)
if err != nil {
return ScrimStart{}, err
}
oldVersion := event.Version
if err = event.Transition([]domain.EventState{domain.RostersConfirmed}, domain.Live, expectedVersion); err != nil {
return ScrimStart{}, err
}
result := ScrimStart{}
seriesToSave := make([]domain.Series, 0)
teamIDs := make([]string, len(roster.Teams))
for i := range roster.Teams {
teamIDs[i] = roster.Teams[i].ID
}
if len(teamIDs) == 2 {
series, createErr := domain.NewSeries(NewID(), eventID, "", [2]string{teamIDs[0], teamIDs[1]}, rules)
if createErr != nil {
return ScrimStart{}, createErr
}
seriesToSave = append(seriesToSave, *series)
event.ActiveSeriesID = series.ID
result.Series = series
} else if len(teamIDs) >= 4 {
tournament, createErr := domain.NewTournament(NewID(), eventID, event.Name, teamIDs)
if createErr != nil {
return ScrimStart{}, createErr
}
for round := range tournament.Rounds {
for match := range tournament.Rounds[round] {
base := tournament.Rounds[round][match]
if base.TeamAID == "" || base.TeamBID == "" {
continue
}
series, seriesErr := domain.NewSeries(base.ID, eventID, tournament.ID, [2]string{base.TeamAID, base.TeamBID}, rules)
if seriesErr != nil {
return ScrimStart{}, seriesErr
}
tournament.Rounds[round][match] = *series
seriesToSave = append(seriesToSave, *series)
}
}
event.TournamentID = tournament.ID
result.Tournament = tournament
} else {
return ScrimStart{}, fmt.Errorf("%w: scrim needs two or at least four teams", domain.ErrInvalid)
}
event.UpdatedAt = s.Now()
err = s.Store.StartScrim(ctx, event, oldVersion, seriesToSave, result.Tournament)
if err == nil {
event, err = s.Store.GetEvent(ctx, event.ID)
}
result.Event = event
if err == nil {
_ = s.Store.AppendAudit(ctx, actor.ID, "scrim.started", eventID, result)
s.Bus.Publish("event:"+eventID, result)
}
return result, err
}
func (s *Service) TossSeriesCoin(ctx context.Context, actor domain.Account, seriesID, seed string, expectedVersion int) (domain.Series, error) {
return s.mutateSeries(ctx, actor, seriesID, expectedVersion, "series.coin_tossed", func(series *domain.Series, rules domain.Ruleset) error {
return series.Toss(seed, actor.ID, s.Now(), rules)
})
}
func (s *Service) BanSeriesMap(ctx context.Context, actor domain.Account, seriesID, teamID, name string, expectedVersion int) (domain.Series, error) {
return s.mutateSeries(ctx, actor, seriesID, expectedVersion, "series.map_banned", func(series *domain.Series, rules domain.Ruleset) error {
return series.BanMap(teamID, name, actor.ID, s.Now(), rules)
})
}
func (s *Service) PickSeriesMap(ctx context.Context, actor domain.Account, seriesID, teamID, name string, expectedVersion int) (domain.Series, error) {
return s.mutateSeries(ctx, actor, seriesID, expectedVersion, "series.map_picked", func(series *domain.Series, rules domain.Ruleset) error {
return series.PickMap(teamID, name, actor.ID, s.Now(), rules)
})
}
func (s *Service) BanSeriesHero(ctx context.Context, actor domain.Account, seriesID, teamID, hero string, expectedVersion int) (domain.Series, error) {
return s.mutateSeries(ctx, actor, seriesID, expectedVersion, "series.hero_banned", func(series *domain.Series, _ domain.Ruleset) error {
return series.BanHero(teamID, hero, actor.ID, s.Now())
})
}
func (s *Service) RecordSeriesResult(ctx context.Context, actor domain.Account, seriesID string, outcome domain.MapOutcome, expectedVersion int) (domain.Series, error) {
out, err := s.mutateSeries(ctx, actor, seriesID, expectedVersion, "series.map_recorded", func(series *domain.Series, rules domain.Ruleset) error {
return series.RecordCurrentMap(outcome, actor.ID, s.Now(), rules)
})
if err != nil || out.WinnerTeamID == "" {
return out, err
}
if out.TournamentID == "" {
event, getErr := s.Store.GetEvent(ctx, out.EventID)
if getErr == nil {
old := event.Version
event.State, event.Version, event.UpdatedAt = domain.Completed, event.Version+1, s.Now()
_, getErr = s.Store.SaveEventWorkflow(ctx, event, old)
}
return out, getErr
}
tournament, err := s.Store.GetTournament(ctx, out.TournamentID)
if err != nil {
return out, err
}
for round := range tournament.Rounds {
for match := range tournament.Rounds[round] {
if tournament.Rounds[round][match].ID != out.ID {
continue
}
tournament.Rounds[round][match] = out
if err = tournament.Advance(round, match); err != nil {
return out, err
}
if round+1 < len(tournament.Rounds) {
next := tournament.Rounds[round+1][match/2]
if next.TeamAID != "" && next.TeamBID != "" {
existing, existingErr := s.Store.GetSeries(ctx, next.ID)
if existingErr == nil {
tournament.Rounds[round+1][match/2] = existing
} else if existingErr == domain.ErrNotFound {
event, _ := s.Store.GetEvent(ctx, out.EventID)
rules, _ := s.Store.GetRuleset(ctx, event.RulesetID)
series, createErr := domain.NewSeries(next.ID, out.EventID, tournament.ID, [2]string{next.TeamAID, next.TeamBID}, rules)
if createErr != nil {
return out, createErr
}
tournament.Rounds[round+1][match/2] = *series
_, err = s.Store.SaveSeries(ctx, *series)
} else {
err = existingErr
}
}
}
}
}
if err == nil {
_, err = s.Store.SaveTournament(ctx, tournament)
s.Bus.Publish("tournament:"+tournament.ID, tournament)
s.Bus.Publish("event:"+out.EventID, tournament)
}
if err == nil && tournament.WinnerTeamID != "" {
event, getErr := s.Store.GetEvent(ctx, out.EventID)
if getErr == nil {
old := event.Version
event.State, event.Version, event.UpdatedAt = domain.Completed, event.Version+1, s.Now()
_, getErr = s.Store.SaveEventWorkflow(ctx, event, old)
}
err = getErr
}
return out, err
}
func (s *Service) mutateSeries(ctx context.Context, actor domain.Account, seriesID string, expectedVersion int, action string, mutation func(*domain.Series, domain.Ruleset) error) (domain.Series, error) {
series, err := s.Store.GetSeries(ctx, seriesID)
if err != nil {
return series, err
}
event, err := s.Store.GetEvent(ctx, series.EventID)
if err != nil {
return series, err
}
if event.State != domain.Live {
return series, fmt.Errorf("%w: event is not live", domain.ErrConflict)
}
if series.Version != expectedVersion {
return series, fmt.Errorf("%w: stale series version", domain.ErrConflict)
}
rules, err := s.Store.GetRuleset(ctx, series.RulesetID)
if err != nil {
return series, err
}
if err = mutation(&series, rules); err != nil {
return series, err
}
series, err = s.Store.SaveSeries(ctx, series)
if err == nil {
_ = s.Store.AppendAudit(ctx, actor.ID, action, seriesID, series)
s.Bus.Publish("series:"+seriesID, series)
}
return series, err
}
func ratingForRole(player domain.Player, role domain.Role) int {
switch role {
case domain.Tank:
return player.Ratings.Tank
case domain.Damage:
return player.Ratings.Damage
case domain.Support:
return player.Ratings.Support
default:
return 0
}
}