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

@@ -63,12 +63,12 @@ func (s *Store) UpsertDiscordAccount(ctx context.Context, account domain.Account
return domain.Account{}, domain.Player{}, err
}
p := domain.Player{ID: application.NewID(), AccountID: account.ID, DisplayName: account.Username, Ratings: domain.Ratings{Tank: 13, Damage: 13, Support: 13}, CreatedAt: account.CreatedAt, UpdatedAt: account.CreatedAt}
var preferredRoles, preferredPlayers []byte
var preferredRoles, preferredPlayers, avoidedPlayers []byte
err = tx.QueryRow(ctx, `INSERT INTO players(id,account_id,display_name,tank_rating,damage_rating,support_rating,created_at,updated_at)
VALUES($1,$2,$3,$4,$5,$6,$7,$8) ON CONFLICT(account_id) DO UPDATE SET account_id=excluded.account_id
RETURNING id,account_id,display_name,tank_rating,damage_rating,support_rating,preferred_roles,preferred_player_ids,created_at,updated_at`,
RETURNING id,account_id,display_name,tank_rating,damage_rating,support_rating,preferred_roles,preferred_player_ids,avoided_player_ids,created_at,updated_at`,
p.ID, p.AccountID, p.DisplayName, p.Ratings.Tank, p.Ratings.Damage, p.Ratings.Support, p.CreatedAt, p.UpdatedAt).
Scan(&p.ID, &p.AccountID, &p.DisplayName, &p.Ratings.Tank, &p.Ratings.Damage, &p.Ratings.Support, &preferredRoles, &preferredPlayers, &p.CreatedAt, &p.UpdatedAt)
Scan(&p.ID, &p.AccountID, &p.DisplayName, &p.Ratings.Tank, &p.Ratings.Damage, &p.Ratings.Support, &preferredRoles, &preferredPlayers, &avoidedPlayers, &p.CreatedAt, &p.UpdatedAt)
if err != nil {
return domain.Account{}, domain.Player{}, err
}
@@ -78,6 +78,9 @@ func (s *Store) UpsertDiscordAccount(ctx context.Context, account domain.Account
if err = json.Unmarshal(preferredPlayers, &p.PreferredPlayerIDs); err != nil {
return domain.Account{}, domain.Player{}, err
}
if err = json.Unmarshal(avoidedPlayers, &p.AvoidedPlayerIDs); err != nil {
return domain.Account{}, domain.Player{}, err
}
return account, p, tx.Commit(ctx)
}
@@ -94,36 +97,40 @@ func (s *Store) DeleteSession(ctx context.Context, token string) error {
func (s *Store) AccountBySession(ctx context.Context, token string) (domain.Account, domain.Player, error) {
var a domain.Account
var p domain.Player
var preferredRoles, preferredPlayers []byte
var preferredRoles, preferredPlayers, avoidedPlayers []byte
err := s.pool.QueryRow(ctx, `SELECT a.id,a.discord_id,a.username,a.avatar_url,a.role,a.created_at,
p.id,p.account_id,p.display_name,p.tank_rating,p.damage_rating,p.support_rating,p.preferred_roles,p.preferred_player_ids,p.created_at,p.updated_at
p.id,p.account_id,p.display_name,p.tank_rating,p.damage_rating,p.support_rating,p.preferred_roles,p.preferred_player_ids,p.avoided_player_ids,p.created_at,p.updated_at
FROM sessions s JOIN accounts a ON a.id=s.account_id JOIN players p ON p.account_id=a.id
WHERE s.token_hash=$1 AND s.expires_at>now()`, HashToken(token)).
Scan(&a.ID, &a.DiscordID, &a.Username, &a.AvatarURL, &a.Role, &a.CreatedAt,
&p.ID, &p.AccountID, &p.DisplayName, &p.Ratings.Tank, &p.Ratings.Damage, &p.Ratings.Support, &preferredRoles, &preferredPlayers, &p.CreatedAt, &p.UpdatedAt)
&p.ID, &p.AccountID, &p.DisplayName, &p.Ratings.Tank, &p.Ratings.Damage, &p.Ratings.Support, &preferredRoles, &preferredPlayers, &avoidedPlayers, &p.CreatedAt, &p.UpdatedAt)
if err == nil {
err = json.Unmarshal(preferredRoles, &p.PreferredRoles)
}
if err == nil {
err = json.Unmarshal(preferredPlayers, &p.PreferredPlayerIDs)
}
if err == nil {
err = json.Unmarshal(avoidedPlayers, &p.AvoidedPlayerIDs)
}
return a, p, mapError(err)
}
func (s *Store) UpdatePlayer(ctx context.Context, p domain.Player) (domain.Player, error) {
preferredRoles, _ := json.Marshal(p.PreferredRoles)
preferredPlayers, _ := json.Marshal(p.PreferredPlayerIDs)
avoidedPlayers, _ := json.Marshal(p.AvoidedPlayerIDs)
err := s.pool.QueryRow(ctx, `UPDATE players SET display_name=$2,tank_rating=$3,damage_rating=$4,support_rating=$5,
preferred_roles=$6,preferred_player_ids=$7,updated_at=$8
preferred_roles=$6,preferred_player_ids=$7,avoided_player_ids=$8,updated_at=$9
WHERE id=$1 RETURNING id,account_id,display_name,tank_rating,damage_rating,support_rating,created_at,updated_at`,
p.ID, p.DisplayName, p.Ratings.Tank, p.Ratings.Damage, p.Ratings.Support, preferredRoles, preferredPlayers, p.UpdatedAt).
p.ID, p.DisplayName, p.Ratings.Tank, p.Ratings.Damage, p.Ratings.Support, preferredRoles, preferredPlayers, avoidedPlayers, p.UpdatedAt).
Scan(&p.ID, &p.AccountID, &p.DisplayName, &p.Ratings.Tank, &p.Ratings.Damage, &p.Ratings.Support, &p.CreatedAt, &p.UpdatedAt)
return p, mapError(err)
}
func (s *Store) ListPlayers(ctx context.Context) ([]domain.Player, error) {
rows, err := s.pool.Query(ctx, `SELECT id,COALESCE(account_id,''),display_name,tank_rating,damage_rating,support_rating,
preferred_roles,preferred_player_ids,created_at,updated_at FROM players ORDER BY id`)
preferred_roles,preferred_player_ids,avoided_player_ids,created_at,updated_at FROM players ORDER BY id`)
if err != nil {
return nil, err
}
@@ -131,9 +138,9 @@ func (s *Store) ListPlayers(ctx context.Context) ([]domain.Player, error) {
out := make([]domain.Player, 0)
for rows.Next() {
var p domain.Player
var preferredRoles, preferredPlayers []byte
var preferredRoles, preferredPlayers, avoidedPlayers []byte
if err := rows.Scan(&p.ID, &p.AccountID, &p.DisplayName, &p.Ratings.Tank, &p.Ratings.Damage, &p.Ratings.Support,
&preferredRoles, &preferredPlayers, &p.CreatedAt, &p.UpdatedAt); err != nil {
&preferredRoles, &preferredPlayers, &avoidedPlayers, &p.CreatedAt, &p.UpdatedAt); err != nil {
return nil, err
}
if err := json.Unmarshal(preferredRoles, &p.PreferredRoles); err != nil {
@@ -142,6 +149,9 @@ func (s *Store) ListPlayers(ctx context.Context) ([]domain.Player, error) {
if err := json.Unmarshal(preferredPlayers, &p.PreferredPlayerIDs); err != nil {
return nil, err
}
if err := json.Unmarshal(avoidedPlayers, &p.AvoidedPlayerIDs); err != nil {
return nil, err
}
out = append(out, p)
}
return out, rows.Err()
@@ -153,18 +163,21 @@ func (s *Store) CreateParticipant(ctx context.Context, p domain.Player, r domain
return domain.Player{}, domain.RSVP{}, err
}
defer tx.Rollback(ctx)
var preferredRoles, preferredPlayers []byte
var preferredRoles, preferredPlayers, avoidedPlayers []byte
err = tx.QueryRow(ctx, `INSERT INTO players(id,account_id,display_name,tank_rating,damage_rating,support_rating,created_at,updated_at)
VALUES($1,NULL,$2,$3,$4,$5,$6,$7)
RETURNING id,COALESCE(account_id,''),display_name,tank_rating,damage_rating,support_rating,preferred_roles,preferred_player_ids,created_at,updated_at`,
RETURNING id,COALESCE(account_id,''),display_name,tank_rating,damage_rating,support_rating,preferred_roles,preferred_player_ids,avoided_player_ids,created_at,updated_at`,
p.ID, p.DisplayName, p.Ratings.Tank, p.Ratings.Damage, p.Ratings.Support, p.CreatedAt, p.UpdatedAt).
Scan(&p.ID, &p.AccountID, &p.DisplayName, &p.Ratings.Tank, &p.Ratings.Damage, &p.Ratings.Support, &preferredRoles, &preferredPlayers, &p.CreatedAt, &p.UpdatedAt)
Scan(&p.ID, &p.AccountID, &p.DisplayName, &p.Ratings.Tank, &p.Ratings.Damage, &p.Ratings.Support, &preferredRoles, &preferredPlayers, &avoidedPlayers, &p.CreatedAt, &p.UpdatedAt)
if err == nil {
err = json.Unmarshal(preferredRoles, &p.PreferredRoles)
}
if err == nil {
err = json.Unmarshal(preferredPlayers, &p.PreferredPlayerIDs)
}
if err == nil {
err = json.Unmarshal(avoidedPlayers, &p.AvoidedPlayerIDs)
}
if err == nil {
err = tx.QueryRow(ctx, `INSERT INTO rsvps(event_id,player_id,status,actor_account_id,source,updated_at)
VALUES($1,$2,$3,$4,$5,$6)
@@ -182,19 +195,21 @@ func (s *Store) CreateParticipant(ctx context.Context, p domain.Player, r domain
}
func (s *Store) CreateEvent(ctx context.Context, e domain.Event) (domain.Event, error) {
err := s.pool.QueryRow(ctx, `INSERT INTO events(id,name,description,starts_at,ends_at,registration_deadline,created_by,created_at,updated_at)
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9)
RETURNING id,name,description,starts_at,ends_at,registration_deadline,created_by,created_at,updated_at`,
e.ID, e.Name, e.Description, e.StartsAt, e.EndsAt, e.RegistrationDeadline, e.CreatedBy, e.CreatedAt, e.UpdatedAt).
Scan(&e.ID, &e.Name, &e.Description, &e.StartsAt, &e.EndsAt, &e.RegistrationDeadline, &e.CreatedBy, &e.CreatedAt, &e.UpdatedAt)
err := s.pool.QueryRow(ctx, `INSERT INTO events(id,name,description,starts_at,ends_at,registration_deadline,created_by,created_at,updated_at,state,version,ruleset_id)
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)
RETURNING id,name,description,starts_at,ends_at,registration_deadline,created_by,created_at,updated_at,state,version,ruleset_id,active_series_id,tournament_id`,
e.ID, e.Name, e.Description, e.StartsAt, e.EndsAt, e.RegistrationDeadline, e.CreatedBy, e.CreatedAt, e.UpdatedAt, e.State, e.Version, e.RulesetID).
Scan(&e.ID, &e.Name, &e.Description, &e.StartsAt, &e.EndsAt, &e.RegistrationDeadline, &e.CreatedBy, &e.CreatedAt, &e.UpdatedAt,
&e.State, &e.Version, &e.RulesetID, &e.ActiveSeriesID, &e.TournamentID)
return e, err
}
func (s *Store) GetEvent(ctx context.Context, id string) (domain.Event, error) {
var event domain.Event
err := s.pool.QueryRow(ctx, `SELECT id,name,description,starts_at,ends_at,registration_deadline,created_by,created_at,updated_at
err := s.pool.QueryRow(ctx, `SELECT id,name,description,starts_at,ends_at,registration_deadline,created_by,created_at,updated_at,state,version,ruleset_id,active_series_id,tournament_id
FROM events WHERE id=$1`, id).
Scan(&event.ID, &event.Name, &event.Description, &event.StartsAt, &event.EndsAt, &event.RegistrationDeadline, &event.CreatedBy, &event.CreatedAt, &event.UpdatedAt)
Scan(&event.ID, &event.Name, &event.Description, &event.StartsAt, &event.EndsAt, &event.RegistrationDeadline, &event.CreatedBy, &event.CreatedAt, &event.UpdatedAt,
&event.State, &event.Version, &event.RulesetID, &event.ActiveSeriesID, &event.TournamentID)
return event, mapError(err)
}
@@ -202,14 +217,15 @@ func (s *Store) UpdateEvent(ctx context.Context, event domain.Event) (domain.Eve
err := s.pool.QueryRow(ctx, `UPDATE events
SET name=$2,description=$3,starts_at=$4,ends_at=$5,registration_deadline=$6,updated_at=$7
WHERE id=$1
RETURNING id,name,description,starts_at,ends_at,registration_deadline,created_by,created_at,updated_at`,
RETURNING id,name,description,starts_at,ends_at,registration_deadline,created_by,created_at,updated_at,state,version,ruleset_id,active_series_id,tournament_id`,
event.ID, event.Name, event.Description, event.StartsAt, event.EndsAt, event.RegistrationDeadline, event.UpdatedAt).
Scan(&event.ID, &event.Name, &event.Description, &event.StartsAt, &event.EndsAt, &event.RegistrationDeadline, &event.CreatedBy, &event.CreatedAt, &event.UpdatedAt)
Scan(&event.ID, &event.Name, &event.Description, &event.StartsAt, &event.EndsAt, &event.RegistrationDeadline, &event.CreatedBy, &event.CreatedAt, &event.UpdatedAt,
&event.State, &event.Version, &event.RulesetID, &event.ActiveSeriesID, &event.TournamentID)
return event, mapError(err)
}
func (s *Store) ListEvents(ctx context.Context, from time.Time) ([]domain.Event, error) {
rows, err := s.pool.Query(ctx, `SELECT id,name,description,starts_at,ends_at,registration_deadline,created_by,created_at,updated_at FROM events WHERE ends_at >= $1 ORDER BY starts_at`, from)
rows, err := s.pool.Query(ctx, `SELECT id,name,description,starts_at,ends_at,registration_deadline,created_by,created_at,updated_at,state,version,ruleset_id,active_series_id,tournament_id FROM events WHERE ends_at >= $1 ORDER BY starts_at`, from)
if err != nil {
return nil, err
}
@@ -217,7 +233,8 @@ func (s *Store) ListEvents(ctx context.Context, from time.Time) ([]domain.Event,
out := make([]domain.Event, 0)
for rows.Next() {
var e domain.Event
if err := rows.Scan(&e.ID, &e.Name, &e.Description, &e.StartsAt, &e.EndsAt, &e.RegistrationDeadline, &e.CreatedBy, &e.CreatedAt, &e.UpdatedAt); err != nil {
if err := rows.Scan(&e.ID, &e.Name, &e.Description, &e.StartsAt, &e.EndsAt, &e.RegistrationDeadline, &e.CreatedBy, &e.CreatedAt, &e.UpdatedAt,
&e.State, &e.Version, &e.RulesetID, &e.ActiveSeriesID, &e.TournamentID); err != nil {
return nil, err
}
out = append(out, e)
@@ -225,6 +242,126 @@ func (s *Store) ListEvents(ctx context.Context, from time.Time) ([]domain.Event,
return out, rows.Err()
}
func (s *Store) SaveEventWorkflow(ctx context.Context, event domain.Event, expectedVersion int) (domain.Event, error) {
tag, err := s.pool.Exec(ctx, `UPDATE events SET state=$2,version=$3,ruleset_id=$4,active_series_id=$5,tournament_id=$6,updated_at=$7
WHERE id=$1 AND version=$8`, event.ID, event.State, event.Version, event.RulesetID, event.ActiveSeriesID, event.TournamentID, event.UpdatedAt, expectedVersion)
if err == nil && tag.RowsAffected() == 0 {
return domain.Event{}, domain.ErrConflict
}
if err != nil {
return domain.Event{}, err
}
return s.GetEvent(ctx, event.ID)
}
func (s *Store) DeleteEvent(ctx context.Context, eventID string) error {
tx, err := s.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
rows, err := tx.Query(ctx, `SELECT p.id FROM players p JOIN rsvps r ON r.player_id=p.id
WHERE r.event_id=$1 AND p.account_id IS NULL`, eventID)
if err != nil {
return err
}
guestIDs := make([]string, 0)
for rows.Next() {
var id string
if err = rows.Scan(&id); err != nil {
rows.Close()
return err
}
guestIDs = append(guestIDs, id)
}
rows.Close()
if err = rows.Err(); err != nil {
return err
}
if _, err = tx.Exec(ctx, `DELETE FROM audit_log WHERE subject_id=$1
OR subject_id IN (SELECT id FROM teams WHERE event_id=$1)
OR subject_id IN (SELECT id FROM series WHERE body->>'eventId'=$1)
OR subject_id IN (SELECT id FROM tournaments WHERE event_id=$1)`, eventID); err != nil {
return err
}
if _, err = tx.Exec(ctx, `DELETE FROM series WHERE body->>'eventId'=$1`, eventID); err != nil {
return err
}
tag, err := tx.Exec(ctx, `DELETE FROM events WHERE id=$1`, eventID)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return domain.ErrNotFound
}
for _, guestID := range guestIDs {
if _, err = tx.Exec(ctx, `DELETE FROM players WHERE id=$1 AND account_id IS NULL
AND NOT EXISTS (SELECT 1 FROM rsvps WHERE player_id=$1)`, guestID); err != nil {
return err
}
}
return tx.Commit(ctx)
}
func (s *Store) SaveRoster(ctx context.Context, roster domain.RosterDraft, expectedVersion int) (domain.RosterDraft, error) {
body, _ := json.Marshal(roster)
var err error
if expectedVersion < 0 {
_, err = s.pool.Exec(ctx, `INSERT INTO event_rosters(event_id,body,version) VALUES($1,$2,$3)
ON CONFLICT(event_id) DO UPDATE SET body=excluded.body,version=excluded.version,updated_at=now()`,
roster.EventID, body, roster.Version)
} else {
tag, execErr := s.pool.Exec(ctx, `UPDATE event_rosters SET body=$2,version=$3,updated_at=now() WHERE event_id=$1 AND version=$4`,
roster.EventID, body, roster.Version, expectedVersion)
err = execErr
if execErr == nil && tag.RowsAffected() == 0 {
err = domain.ErrConflict
}
}
return roster, err
}
func (s *Store) GetRoster(ctx context.Context, eventID string) (domain.RosterDraft, error) {
var roster domain.RosterDraft
var body []byte
err := s.pool.QueryRow(ctx, `SELECT body FROM event_rosters WHERE event_id=$1`, eventID).Scan(&body)
if err == nil {
err = json.Unmarshal(body, &roster)
}
return roster, mapError(err)
}
func (s *Store) StartScrim(ctx context.Context, event domain.Event, expectedVersion int, series []domain.Series, tournament *domain.Tournament) error {
tx, err := s.pool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
for _, item := range series {
body, _ := json.Marshal(item)
if _, err = tx.Exec(ctx, `INSERT INTO series(id,tournament_id,body,version) VALUES($1,$2,$3,$4)`,
item.ID, item.TournamentID, body, item.Version); err != nil {
return mapError(err)
}
}
if tournament != nil {
body, _ := json.Marshal(tournament)
if _, err = tx.Exec(ctx, `INSERT INTO tournaments(id,event_id,body) VALUES($1,$2,$3)`,
tournament.ID, tournament.EventID, body); err != nil {
return mapError(err)
}
}
tag, err := tx.Exec(ctx, `UPDATE events SET state=$2,version=$3,active_series_id=$4,tournament_id=$5,updated_at=$6
WHERE id=$1 AND version=$7`, event.ID, event.State, event.Version, event.ActiveSeriesID, event.TournamentID, event.UpdatedAt, expectedVersion)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return domain.ErrConflict
}
return tx.Commit(ctx)
}
func (s *Store) UpsertRSVP(ctx context.Context, r domain.RSVP) (domain.RSVP, error) {
err := s.pool.QueryRow(ctx, `INSERT INTO rsvps(event_id,player_id,status,actor_account_id,source,updated_at)
VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT(event_id,player_id) DO UPDATE
@@ -252,6 +389,14 @@ func (s *Store) ListRSVPs(ctx context.Context, eventID string) ([]domain.RSVP, e
return out, rows.Err()
}
func (s *Store) DeleteRSVP(ctx context.Context, eventID, playerID string) error {
tag, err := s.pool.Exec(ctx, `DELETE FROM rsvps WHERE event_id=$1 AND player_id=$2`, eventID, playerID)
if err == nil && tag.RowsAffected() == 0 {
return domain.ErrNotFound
}
return err
}
func (s *Store) SaveTeams(ctx context.Context, eventID string, teams []domain.Team) error {
tx, err := s.pool.Begin(ctx)
if err != nil {

View File

@@ -2,10 +2,19 @@ package postgres
import (
"context"
"fmt"
"os"
"testing"
"time"
"mixmaker/backend/internal/application"
"mixmaker/backend/internal/domain"
)
type discardPublisher struct{}
func (discardPublisher) Publish(string, any) {}
func TestMigrationsAndReadiness(t *testing.T) {
databaseURL := os.Getenv("TEST_DATABASE_URL")
if databaseURL == "" {
@@ -23,4 +32,177 @@ func TestMigrationsAndReadiness(t *testing.T) {
if err := store.Ready(ctx); err != nil {
t.Fatal(err)
}
var workflowColumns int
if err := store.pool.QueryRow(ctx, `SELECT count(*) FROM information_schema.columns
WHERE table_name='events' AND column_name IN ('state','version','ruleset_id','active_series_id','tournament_id')`).Scan(&workflowColumns); err != nil {
t.Fatal(err)
}
if workflowColumns != 5 {
t.Fatalf("workflow migration is incomplete: found %d event columns", workflowColumns)
}
var rosterTable bool
if err := store.pool.QueryRow(ctx, `SELECT to_regclass('public.event_rosters') IS NOT NULL`).Scan(&rosterTable); err != nil {
t.Fatal(err)
}
if !rosterTable {
t.Fatal("event_rosters table is missing")
}
}
func TestFullScrimPipeline(t *testing.T) {
databaseURL := os.Getenv("TEST_DATABASE_URL")
if databaseURL == "" {
t.Skip("TEST_DATABASE_URL is not configured")
}
ctx := context.Background()
if err := Migrate(ctx, databaseURL, "../../../migrations"); err != nil {
t.Fatal(err)
}
store, err := Open(ctx, databaseURL)
if err != nil {
t.Fatal(err)
}
defer store.Close()
suffix := fmt.Sprintf("%d", time.Now().UnixNano())
accountID, eventID := "pipeline-admin-"+suffix, "pipeline-event-"+suffix
playerIDs, seriesIDs := make([]string, 0, 10), make([]string, 0, 1)
_, err = store.pool.Exec(ctx, `INSERT INTO accounts(id,discord_id,username,avatar_url,role,created_at)
VALUES($1,$2,'Pipeline Admin','','admin',now())`, accountID, accountID)
if err != nil {
t.Fatal(err)
}
defer func() {
_, _ = store.pool.Exec(ctx, `DELETE FROM audit_log WHERE actor_account_id=$1`, accountID)
_, _ = store.pool.Exec(ctx, `DELETE FROM events WHERE id=$1`, eventID)
for _, seriesID := range seriesIDs {
_, _ = store.pool.Exec(ctx, `DELETE FROM series WHERE id=$1`, seriesID)
}
for _, playerID := range playerIDs {
_, _ = store.pool.Exec(ctx, `DELETE FROM players WHERE id=$1`, playerID)
}
_, _ = store.pool.Exec(ctx, `DELETE FROM accounts WHERE id=$1`, accountID)
}()
service := application.New(store, discardPublisher{})
admin := domain.Account{ID: accountID, Role: domain.RoleAdmin}
now := time.Now().UTC()
event, err := service.CreateEvent(ctx, admin, domain.Event{
ID: eventID, Name: "Pipeline", StartsAt: now.Add(24 * time.Hour), EndsAt: now.Add(28 * time.Hour),
RegistrationDeadline: now.Add(23 * time.Hour), RulesetID: "standard-control-hybrid-control",
})
if err != nil {
t.Fatal(err)
}
// CreateEvent owns the identifier, so use the returned value from here onward.
eventID = event.ID
for i := 0; i < 10; i++ {
player, _, createErr := service.CreateParticipant(ctx, admin, eventID, fmt.Sprintf("Player %d", i+1),
domain.Ratings{Tank: 18 + i%5, Damage: 19 + i%5, Support: 20 + i%5}, domain.Going)
if createErr != nil {
t.Fatal(createErr)
}
playerIDs = append(playerIDs, player.ID)
}
event, err = service.CloseRegistration(ctx, admin, eventID, event.RulesetID, event.Version)
if err != nil {
t.Fatal(err)
}
balance, err := service.GenerateBalance(ctx, admin, eventID, event.Version)
if err != nil || len(balance.Candidates) == 0 {
t.Fatalf("balance failed: candidates=%d err=%v", len(balance.Candidates), err)
}
roster, err := service.SelectWorkflowBalance(ctx, admin, eventID, balance.Candidates[0], balance.Event.Version)
if err != nil {
t.Fatal(err)
}
for _, team := range roster.Teams {
roster, err = service.SetRosterCaptain(ctx, admin, eventID, team.ID, team.Slots[0].PlayerID, roster.Version)
if err != nil {
t.Fatal(err)
}
}
event, err = store.GetEvent(ctx, eventID)
if err != nil {
t.Fatal(err)
}
event, err = service.ConfirmRosters(ctx, admin, eventID, event.Version, roster.Version)
if err != nil {
t.Fatal(err)
}
started, err := service.StartScrim(ctx, admin, eventID, event.Version)
if err != nil || started.Series == nil {
t.Fatalf("start failed: %#v, %v", started, err)
}
seriesIDs = append(seriesIDs, started.Series.ID)
series, err := service.TossSeriesCoin(ctx, admin, started.Series.ID, "integration-seed", started.Series.Version)
if err != nil {
t.Fatal(err)
}
for mapNumber := 0; mapNumber < 2; mapNumber++ {
if series.Phase == domain.MapPickPhase {
series, err = service.PickSeriesMap(ctx, admin, series.ID, series.NextMapPickerID, series.AvailableMaps[0], series.Version)
if err != nil {
t.Fatal(err)
}
}
for series.Phase != domain.HeroBanPhase {
name := firstUnbanned(series.MapDraft.Pool, series.MapDraft.Banned)
series, err = service.BanSeriesMap(ctx, admin, series.ID, series.MapDraft.NextTeam(), name, series.Version)
if err != nil {
t.Fatal(err)
}
}
for series.Phase != domain.PlayingPhase {
teamID := series.HeroDraft.NextTeam()
hero := firstAvailableHero(series.HeroDraft, teamID)
series, err = service.BanSeriesHero(ctx, admin, series.ID, teamID, hero, series.Version)
if err != nil {
t.Fatal(err)
}
}
series, err = service.RecordSeriesResult(ctx, admin, series.ID, domain.TeamAWin, series.Version)
if err != nil {
t.Fatal(err)
}
}
if series.Phase != domain.SeriesComplete {
t.Fatalf("series did not complete: %s", series.Phase)
}
event, err = store.GetEvent(ctx, eventID)
if err != nil || event.State != domain.Completed {
t.Fatalf("event did not complete: state=%s err=%v", event.State, err)
}
}
func firstUnbanned(pool, banned []string) string {
for _, name := range pool {
found := false
for _, item := range banned {
found = found || item == name
}
if !found {
return name
}
}
return ""
}
func firstAvailableHero(draft *domain.HeroDraft, teamID string) string {
for _, hero := range draft.Heroes {
usedRole, usedHero, ownRepeat := false, false, false
for _, role := range draft.CurrentRoles[teamID] {
usedRole = usedRole || role == hero.Role
}
for _, ban := range draft.CurrentBans {
usedHero = usedHero || ban.Value == hero.Name
}
for _, name := range draft.SeriesBans[teamID] {
ownRepeat = ownRepeat || name == hero.Name
}
if !usedRole && !usedHero && !ownRepeat {
return hero.Name
}
}
return ""
}