669 lines
22 KiB
Go
669 lines
22 KiB
Go
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.IsStaff() {
|
|
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.IsStaff() {
|
|
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.IsStaff() {
|
|
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.IsStaff() {
|
|
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.IsStaff() {
|
|
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.IsStaff() {
|
|
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)
|
|
}
|
|
roleA, roleB := rosterPlayerRole(roster, teamA, playerA), rosterPlayerRole(roster, teamB, playerB)
|
|
players, err := s.Store.ListPlayers(ctx)
|
|
if err != nil {
|
|
return roster, err
|
|
}
|
|
ratingAForB, ratingBForA := 0, 0
|
|
for _, player := range players {
|
|
if player.ID == playerA {
|
|
ratingAForB = ratingForRole(player, roleB)
|
|
}
|
|
if player.ID == playerB {
|
|
ratingBForA = ratingForRole(player, roleA)
|
|
}
|
|
}
|
|
if ratingAForB == 0 || ratingBForA == 0 {
|
|
return roster, domain.ErrNotFound
|
|
}
|
|
if err = roster.SwapAcrossRoles(teamA, playerA, teamB, playerB, ratingBForA, ratingAForB); 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) MoveRosterPlayer(ctx context.Context, actor domain.Account, eventID, fromTeamID, playerID, toTeamID string, role domain.Role, expectedVersion int) (domain.RosterDraft, error) {
|
|
if !actor.IsStaff() {
|
|
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)
|
|
}
|
|
players, err := s.Store.ListPlayers(ctx)
|
|
if err != nil {
|
|
return roster, err
|
|
}
|
|
targetRating := 0
|
|
for _, player := range players {
|
|
if player.ID == playerID {
|
|
targetRating = ratingForRole(player, role)
|
|
break
|
|
}
|
|
}
|
|
if targetRating == 0 {
|
|
return roster, domain.ErrNotFound
|
|
}
|
|
if err = roster.MoveToEmpty(fromTeamID, playerID, toTeamID, role, targetRating); err != nil {
|
|
return roster, err
|
|
}
|
|
return s.saveRosterChange(ctx, actor, roster, expectedVersion, "roster.player_moved")
|
|
}
|
|
|
|
func (s *Service) PlaceReservePlayer(ctx context.Context, actor domain.Account, eventID, teamID, reserveID string, role domain.Role, expectedVersion int) (domain.RosterDraft, error) {
|
|
if !actor.IsStaff() {
|
|
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)
|
|
}
|
|
players, err := s.Store.ListPlayers(ctx)
|
|
if err != nil {
|
|
return roster, err
|
|
}
|
|
rating := 0
|
|
for _, player := range players {
|
|
if player.ID == reserveID {
|
|
rating = ratingForRole(player, role)
|
|
break
|
|
}
|
|
}
|
|
if rating == 0 {
|
|
return roster, domain.ErrNotFound
|
|
}
|
|
if err = roster.PlaceReserve(teamID, role, reserveID, rating); err != nil {
|
|
return roster, err
|
|
}
|
|
return s.saveRosterChange(ctx, actor, roster, expectedVersion, "roster.reserve_placed")
|
|
}
|
|
|
|
func (s *Service) RemoveRosterPlayer(ctx context.Context, actor domain.Account, eventID, teamID, playerID string, expectedVersion int) (domain.RosterDraft, error) {
|
|
if !actor.IsStaff() {
|
|
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.MoveToReserve(teamID, playerID); err != nil {
|
|
return roster, err
|
|
}
|
|
return s.saveRosterChange(ctx, actor, roster, expectedVersion, "roster.player_removed")
|
|
}
|
|
|
|
func (s *Service) saveRosterChange(ctx context.Context, actor domain.Account, roster domain.RosterDraft, expectedVersion int, action string) (domain.RosterDraft, error) {
|
|
out, err := s.Store.SaveRoster(ctx, roster, expectedVersion)
|
|
if err == nil {
|
|
_ = s.Store.SaveTeams(ctx, roster.EventID, out.Teams)
|
|
_ = s.Store.AppendAudit(ctx, actor.ID, action, roster.EventID, out)
|
|
s.Bus.Publish("event:"+roster.EventID, out)
|
|
}
|
|
return out, 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.IsStaff() {
|
|
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.IsStaff() {
|
|
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.IsStaff() {
|
|
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) RevertWorkflowStage(ctx context.Context, actor domain.Account, eventID string, expectedVersion int) (domain.Event, error) {
|
|
if !actor.IsStaff() {
|
|
return domain.Event{}, domain.ErrForbidden
|
|
}
|
|
event, err := s.Store.GetEvent(ctx, eventID)
|
|
if err != nil {
|
|
return event, err
|
|
}
|
|
oldVersion := event.Version
|
|
var previous domain.EventState
|
|
switch event.State {
|
|
case domain.RegistrationClosed:
|
|
previous = domain.RegistrationOpen
|
|
case domain.Balancing:
|
|
previous = domain.RegistrationClosed
|
|
case domain.RostersDraft:
|
|
previous = domain.Balancing
|
|
case domain.RostersConfirmed:
|
|
previous = domain.RostersDraft
|
|
default:
|
|
return event, fmt.Errorf("%w: this workflow stage cannot be reverted", domain.ErrConflict)
|
|
}
|
|
if err = event.Transition([]domain.EventState{event.State}, previous, expectedVersion); err != nil {
|
|
return event, err
|
|
}
|
|
if previous == domain.Balancing {
|
|
if err = s.Store.ResetRoster(ctx, eventID); err != nil {
|
|
return event, err
|
|
}
|
|
}
|
|
if previous == domain.RostersDraft {
|
|
roster, rosterErr := s.Store.GetRoster(ctx, eventID)
|
|
if rosterErr != nil {
|
|
return event, rosterErr
|
|
}
|
|
rosterVersion := roster.Version
|
|
roster.Confirmed = false
|
|
roster.Version++
|
|
if _, rosterErr = s.Store.SaveRoster(ctx, roster, rosterVersion); rosterErr != nil {
|
|
return event, rosterErr
|
|
}
|
|
}
|
|
event.UpdatedAt = s.Now()
|
|
event, err = s.Store.SaveEventWorkflow(ctx, event, oldVersion)
|
|
if err == nil {
|
|
_ = s.Store.AppendAudit(ctx, actor.ID, "workflow.reverted", eventID, map[string]domain.EventState{"state": previous})
|
|
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.IsStaff() {
|
|
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
|
|
}
|
|
}
|
|
|
|
func rosterPlayerRole(roster domain.RosterDraft, teamID, playerID string) domain.Role {
|
|
for _, team := range roster.Teams {
|
|
if team.ID != teamID {
|
|
continue
|
|
}
|
|
for _, slot := range team.Slots {
|
|
if slot.PlayerID == playerID {
|
|
return slot.Role
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|