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

@@ -69,13 +69,26 @@ func New(service *application.Service, store application.Store, hub *realtime.Hu
api.Post("/api/events", s.createEvent)
api.Get("/api/events/{eventID}", s.getEvent)
api.Put("/api/events/{eventID}", s.updateEvent)
api.Delete("/api/events/{eventID}", s.deleteEvent)
api.Post("/api/events/{eventID}/cancel", s.cancelEvent)
api.Get("/api/events/{eventID}/tournament", s.getEventTournament)
api.Get("/api/events/{eventID}/rsvps", s.rsvps)
api.Post("/api/events/{eventID}/participants", s.createParticipant)
api.Put("/api/events/{eventID}/rsvps/{playerID}", s.setRSVP)
api.Delete("/api/events/{eventID}/rsvps/{playerID}", s.removeParticipant)
api.Post("/api/events/{eventID}/balance", s.balance)
api.Put("/api/events/{eventID}/teams", s.selectBalance)
api.Get("/api/events/{eventID}/teams", s.teams)
api.Post("/api/events/{eventID}/registration/close", s.closeRegistration)
api.Post("/api/events/{eventID}/balance/generate", s.generateWorkflowBalance)
api.Get("/api/events/{eventID}/roster", s.getRoster)
api.Put("/api/events/{eventID}/roster", s.selectWorkflowBalance)
api.Post("/api/events/{eventID}/roster/swap", s.swapRoster)
api.Post("/api/events/{eventID}/roster/substitute", s.substituteRoster)
api.Post("/api/events/{eventID}/roster/emergency-substitute", s.emergencySubstitute)
api.Put("/api/events/{eventID}/roster/captain", s.setRosterCaptain)
api.Post("/api/events/{eventID}/roster/confirm", s.confirmRosters)
api.Post("/api/events/{eventID}/start", s.startScrim)
api.Put("/api/teams/{teamID}/captain", s.assignCaptain)
api.Post("/api/rulesets", s.saveRuleset)
api.Get("/api/rulesets", s.listRulesets)
@@ -88,6 +101,11 @@ func New(service *application.Service, store application.Store, hub *realtime.Hu
api.Post("/api/tournaments", s.createTournament)
api.Get("/api/tournaments/{id}", s.getTournament)
api.Get("/api/series/{id}", s.getSeries)
api.Post("/api/series/{id}/coin-toss", s.tossSeriesCoin)
api.Post("/api/series/{id}/map-bans", s.banSeriesMap)
api.Post("/api/series/{id}/map-pick", s.pickSeriesMap)
api.Post("/api/series/{id}/hero-bans", s.banSeriesHero)
api.Post("/api/series/{id}/map-result", s.recordSeriesResult)
api.Post("/api/series/{id}/results", s.recordResult)
api.Patch("/api/series/{id}/results/{index}", s.correctResult)
api.Get("/api/events/stream", s.stream)
@@ -220,12 +238,13 @@ func (s *Server) updateProfile(w http.ResponseWriter, r *http.Request) {
Ratings domain.Ratings `json:"ratings"`
PreferredRoles []domain.Role `json:"preferredRoles"`
PreferredPlayerIDs []string `json:"preferredPlayerIds"`
AvoidedPlayerIDs []string `json:"avoidedPlayerIds"`
}
if !decode(w, r, &in) {
return
}
id := who(r)
out, err := s.service.UpdateOwnProfile(r.Context(), id.account, id.player, in.DisplayName, in.Ratings, in.PreferredRoles, in.PreferredPlayerIDs)
out, err := s.service.UpdateOwnProfile(r.Context(), id.account, id.player, in.DisplayName, in.Ratings, in.PreferredRoles, in.PreferredPlayerIDs, in.AvoidedPlayerIDs)
respond(w, out, err, http.StatusOK)
}
@@ -235,7 +254,7 @@ func (s *Server) players(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) events(w http.ResponseWriter, r *http.Request) {
from := time.Now().UTC()
from := time.Unix(0, 0).UTC()
if raw := r.URL.Query().Get("from"); raw != "" {
if parsed, err := time.Parse(time.RFC3339, raw); err == nil {
from = parsed
@@ -303,6 +322,11 @@ func (s *Server) setRSVP(w http.ResponseWriter, r *http.Request) {
respond(w, out, err, 200)
}
func (s *Server) removeParticipant(w http.ResponseWriter, r *http.Request) {
err := s.service.RemoveParticipant(r.Context(), who(r).account, chi.URLParam(r, "eventID"), chi.URLParam(r, "playerID"))
respond(w, nil, err, http.StatusNoContent)
}
func (s *Server) balance(w http.ResponseWriter, r *http.Request) {
out, err := s.service.Balance(r.Context(), who(r).account, chi.URLParam(r, "eventID"))
respond(w, out, err, 200)

View File

@@ -0,0 +1,243 @@
package httpapi
import (
"net/http"
"github.com/go-chi/chi/v5"
"mixmaker/backend/internal/domain"
)
func (s *Server) cancelEvent(w http.ResponseWriter, r *http.Request) {
var in struct {
ExpectedVersion int `json:"expectedVersion"`
}
if !decode(w, r, &in) {
return
}
out, err := s.service.CancelEvent(r.Context(), who(r).account, chi.URLParam(r, "eventID"), in.ExpectedVersion)
respond(w, out, err, http.StatusOK)
}
func (s *Server) deleteEvent(w http.ResponseWriter, r *http.Request) {
err := s.service.DeleteEvent(r.Context(), who(r).account, chi.URLParam(r, "eventID"))
respond(w, nil, err, http.StatusNoContent)
}
func (s *Server) closeRegistration(w http.ResponseWriter, r *http.Request) {
var in struct {
RulesetID string `json:"rulesetId"`
ExpectedVersion int `json:"expectedVersion"`
}
if !decode(w, r, &in) {
return
}
out, err := s.service.CloseRegistration(r.Context(), who(r).account, chi.URLParam(r, "eventID"), in.RulesetID, in.ExpectedVersion)
respond(w, out, err, http.StatusOK)
}
func (s *Server) generateWorkflowBalance(w http.ResponseWriter, r *http.Request) {
var in struct {
ExpectedVersion int `json:"expectedVersion"`
}
if !decode(w, r, &in) {
return
}
out, err := s.service.GenerateBalance(r.Context(), who(r).account, chi.URLParam(r, "eventID"), in.ExpectedVersion)
respond(w, out, err, http.StatusOK)
}
func (s *Server) getRoster(w http.ResponseWriter, r *http.Request) {
out, err := s.store.GetRoster(r.Context(), chi.URLParam(r, "eventID"))
respond(w, out, err, http.StatusOK)
}
func (s *Server) selectWorkflowBalance(w http.ResponseWriter, r *http.Request) {
var in struct {
Candidate domain.BalanceCandidate `json:"candidate"`
ExpectedVersion int `json:"expectedVersion"`
}
if !decode(w, r, &in) {
return
}
out, err := s.service.SelectWorkflowBalance(r.Context(), who(r).account, chi.URLParam(r, "eventID"), in.Candidate, in.ExpectedVersion)
respond(w, out, err, http.StatusOK)
}
func (s *Server) swapRoster(w http.ResponseWriter, r *http.Request) {
var in struct {
TeamAID string `json:"teamAId"`
PlayerAID string `json:"playerAId"`
TeamBID string `json:"teamBId"`
PlayerBID string `json:"playerBId"`
ExpectedVersion int `json:"expectedVersion"`
}
if !decode(w, r, &in) {
return
}
out, err := s.service.SwapRoster(r.Context(), who(r).account, chi.URLParam(r, "eventID"), in.TeamAID, in.PlayerAID, in.TeamBID, in.PlayerBID, in.ExpectedVersion)
respond(w, out, err, http.StatusOK)
}
func (s *Server) substituteRoster(w http.ResponseWriter, r *http.Request) {
s.handleSubstitute(w, r, false)
}
func (s *Server) emergencySubstitute(w http.ResponseWriter, r *http.Request) {
s.handleSubstitute(w, r, true)
}
func (s *Server) handleSubstitute(w http.ResponseWriter, r *http.Request, emergency bool) {
var in struct {
TeamID string `json:"teamId"`
OutgoingPlayerID string `json:"outgoingPlayerId"`
ReservePlayerID string `json:"reservePlayerId"`
ExpectedVersion int `json:"expectedVersion"`
}
if !decode(w, r, &in) {
return
}
out, err := s.service.SubstituteRoster(r.Context(), who(r).account, chi.URLParam(r, "eventID"), in.TeamID, in.OutgoingPlayerID, in.ReservePlayerID, in.ExpectedVersion, emergency)
respond(w, out, err, http.StatusOK)
}
func (s *Server) setRosterCaptain(w http.ResponseWriter, r *http.Request) {
var in struct {
TeamID string `json:"teamId"`
PlayerID string `json:"playerId"`
ExpectedVersion int `json:"expectedVersion"`
}
if !decode(w, r, &in) {
return
}
out, err := s.service.SetRosterCaptain(r.Context(), who(r).account, chi.URLParam(r, "eventID"), in.TeamID, in.PlayerID, in.ExpectedVersion)
respond(w, out, err, http.StatusOK)
}
func (s *Server) confirmRosters(w http.ResponseWriter, r *http.Request) {
var in struct {
ExpectedVersion int `json:"expectedVersion"`
ExpectedRosterVersion int `json:"expectedRosterVersion"`
}
if !decode(w, r, &in) {
return
}
out, err := s.service.ConfirmRosters(r.Context(), who(r).account, chi.URLParam(r, "eventID"), in.ExpectedVersion, in.ExpectedRosterVersion)
respond(w, out, err, http.StatusOK)
}
func (s *Server) startScrim(w http.ResponseWriter, r *http.Request) {
var in struct {
ExpectedVersion int `json:"expectedVersion"`
}
if !decode(w, r, &in) {
return
}
out, err := s.service.StartScrim(r.Context(), who(r).account, chi.URLParam(r, "eventID"), in.ExpectedVersion)
respond(w, out, err, http.StatusCreated)
}
func (s *Server) tossSeriesCoin(w http.ResponseWriter, r *http.Request) {
var in struct {
Seed string `json:"seed"`
ActingTeamID string `json:"actingTeamId"`
ExpectedVersion int `json:"expectedVersion"`
}
if !decode(w, r, &in) {
return
}
series, err := s.store.GetSeries(r.Context(), chi.URLParam(r, "id"))
if err == nil {
err = s.authorizeTeam(r, series.EventID, in.ActingTeamID)
}
if err != nil {
writeError(w, err)
return
}
out, err := s.service.TossSeriesCoin(r.Context(), who(r).account, series.ID, in.Seed, in.ExpectedVersion)
respond(w, out, err, http.StatusOK)
}
func (s *Server) banSeriesMap(w http.ResponseWriter, r *http.Request) {
var in struct {
TeamID string `json:"teamId"`
Map string `json:"map"`
ExpectedVersion int `json:"expectedVersion"`
}
if !decode(w, r, &in) {
return
}
series, err := s.store.GetSeries(r.Context(), chi.URLParam(r, "id"))
if err == nil {
err = s.authorizeTeam(r, series.EventID, in.TeamID)
}
if err != nil {
writeError(w, err)
return
}
out, err := s.service.BanSeriesMap(r.Context(), who(r).account, series.ID, in.TeamID, in.Map, in.ExpectedVersion)
respond(w, out, err, http.StatusOK)
}
func (s *Server) pickSeriesMap(w http.ResponseWriter, r *http.Request) {
var in struct {
TeamID string `json:"teamId"`
Map string `json:"map"`
ExpectedVersion int `json:"expectedVersion"`
}
if !decode(w, r, &in) {
return
}
series, err := s.store.GetSeries(r.Context(), chi.URLParam(r, "id"))
if err == nil {
err = s.authorizeTeam(r, series.EventID, in.TeamID)
}
if err != nil {
writeError(w, err)
return
}
out, err := s.service.PickSeriesMap(r.Context(), who(r).account, series.ID, in.TeamID, in.Map, in.ExpectedVersion)
respond(w, out, err, http.StatusOK)
}
func (s *Server) banSeriesHero(w http.ResponseWriter, r *http.Request) {
var in struct {
TeamID string `json:"teamId"`
Hero string `json:"hero"`
ExpectedVersion int `json:"expectedVersion"`
}
if !decode(w, r, &in) {
return
}
series, err := s.store.GetSeries(r.Context(), chi.URLParam(r, "id"))
if err == nil {
err = s.authorizeTeam(r, series.EventID, in.TeamID)
}
if err != nil {
writeError(w, err)
return
}
out, err := s.service.BanSeriesHero(r.Context(), who(r).account, series.ID, in.TeamID, in.Hero, in.ExpectedVersion)
respond(w, out, err, http.StatusOK)
}
func (s *Server) recordSeriesResult(w http.ResponseWriter, r *http.Request) {
var in struct {
Outcome domain.MapOutcome `json:"outcome"`
ActingTeamID string `json:"actingTeamId"`
ExpectedVersion int `json:"expectedVersion"`
}
if !decode(w, r, &in) {
return
}
series, err := s.store.GetSeries(r.Context(), chi.URLParam(r, "id"))
if err == nil {
err = s.authorizeTeam(r, series.EventID, in.ActingTeamID)
}
if err != nil {
writeError(w, err)
return
}
out, err := s.service.RecordSeriesResult(r.Context(), who(r).account, series.ID, in.Outcome, in.ExpectedVersion)
respond(w, out, err, http.StatusOK)
}