generated from VLADIMIR/template
add
This commit is contained in:
@@ -35,7 +35,9 @@ func TestFinishedTeamStoryPostgres(t *testing.T) {
|
||||
CREATE TEMP TABLE teams (id INT, name TEXT, captain_email TEXT, participants_count INT,
|
||||
status TEXT, password TEXT, game_id INT);
|
||||
INSERT INTO games VALUES (1, 'Game', '', NOW(), 'online', 'finish', 1, NULL, NULL);
|
||||
INSERT INTO teams VALUES (1, 'Team', '', 1, 'register', 'secret', 1);`)
|
||||
INSERT INTO teams VALUES (1, 'Team', '', 1, 'register', 'secret', 1);
|
||||
ALTER TABLE teams ADD COLUMN participation_status TEXT DEFAULT 'pending', ADD COLUMN participation_source TEXT DEFAULT '',
|
||||
ADD COLUMN participation_email_error TEXT DEFAULT '', ADD COLUMN participation_sent_at TIMESTAMPTZ;`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package game_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"log"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
_ "time/tzdata"
|
||||
|
||||
"evening_detective_server/internal/modules/email_sender"
|
||||
"evening_detective_server/internal/repos/teams_repo"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
func (s *GameService) GetParticipation(ctx context.Context, token string) (*teams_repo.Participation, error) {
|
||||
return s.teamsRepo.GetParticipation(ctx, token)
|
||||
}
|
||||
func (s *GameService) AnswerParticipation(ctx context.Context, token, answer string) (*teams_repo.Participation, error) {
|
||||
return s.teamsRepo.AnswerParticipation(ctx, token, answer)
|
||||
}
|
||||
|
||||
// Stable, purpose-separated tokens keep links valid when an ambiguous SMTP failure is retried.
|
||||
// Only the hash is stored in the database; the signing secret must stay stable across workers/restarts.
|
||||
func participationToken(secret string, id int) string {
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
fmt.Fprintf(mac, "team-participation:v1:%d", id)
|
||||
return hex.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
func participationEmail(p *teams_repo.Participation, baseURL, token string, location *time.Location) email_sender.Message {
|
||||
link := strings.TrimRight(baseURL, "/") + "/participation#token=" + token
|
||||
date := p.StartAt.In(location).Format("02.01.2006 в 15:04 (MST, UTC-07:00)")
|
||||
body := fmt.Sprintf("Команда «%s», подтвердите участие в игре «%s».\nДата: %s\nУчастников: %d\n\nПодтвердить участие: %s&answer=confirmed\nОтменить участие: %s&answer=cancelled\n\nОтвет можно дать до начала игры. Решение окончательное: изменить его нельзя. Без ответа запись сохраняется со статусом «Ожидает ответа».\nНе пересылайте письмо: ссылка позволяет принять решение за команду.", p.TeamName, p.GameName, date, p.ParticipantsCount, link, link)
|
||||
markup := "<!doctype html><html lang=\"ru\"><meta charset=\"utf-8\"><body><h2>Подтвердите участие в игре</h2><p>" + html.EscapeString(p.TeamName) + " — «" + html.EscapeString(p.GameName) + "»</p><p>" + html.EscapeString(date) + fmt.Sprintf(" · Участников: %d</p>", p.ParticipantsCount)
|
||||
for _, action := range []struct{ value, label string }{{"confirmed", "Подтвердить участие"}, {"cancelled", "Отменить участие"}} {
|
||||
markup += `<p><a href="` + html.EscapeString(link+"&answer="+action.value) + `" style="display:inline-block;padding:12px 20px;background:#342a22;color:#fff;border-radius:5px">` + action.label + `</a></p>`
|
||||
}
|
||||
markup += "<p>Ответ можно дать до начала игры. <strong>Решение окончательное: изменить его нельзя.</strong></p><p>Без ответа запись сохраняется со статусом «Ожидает ответа».</p><p>Не пересылайте письмо: ссылка позволяет принять решение за команду.</p></body></html>"
|
||||
return email_sender.Message{To: p.Email, Subject: "Подтвердите участие в «Вечернем детективе»", Body: body, HTML: markup}
|
||||
}
|
||||
|
||||
func (s *GameService) RunParticipationEmails(ctx context.Context, sender email_sender.IEmailSender, secret, timezone string) {
|
||||
base, err := url.Parse(s.appBaseURL)
|
||||
if err != nil || base.Host == "" || (base.Scheme != "https" && base.Scheme != "http") || base.User != nil || base.RawQuery != "" || base.Fragment != "" || secret == "" {
|
||||
log.Print("participation email worker disabled: configure APP_BASE_URL and PARTICIPATION_SECRET")
|
||||
return
|
||||
}
|
||||
if timezone == "" {
|
||||
timezone = "Asia/Barnaul"
|
||||
}
|
||||
location, err := time.LoadLocation(timezone)
|
||||
if err != nil {
|
||||
log.Printf("participation email worker disabled: invalid GAME_TIMEZONE: %v", err)
|
||||
return
|
||||
}
|
||||
ticker := time.NewTicker(time.Minute)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
if err := s.sendParticipationEmails(ctx, sender, secret, location); err != nil && ctx.Err() == nil {
|
||||
log.Printf("participation email worker: %v", err)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GameService) sendParticipationEmails(ctx context.Context, sender email_sender.IEmailSender, secret string, location *time.Location) error {
|
||||
for ctx.Err() == nil {
|
||||
jobCtx, cancel := context.WithTimeout(ctx, time.Minute)
|
||||
p, err := s.teamsRepo.ClaimParticipationEmail(jobCtx)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
cancel()
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
token := participationToken(secret, p.TeamID)
|
||||
err = s.teamsRepo.SetParticipationToken(jobCtx, p.TeamID, token)
|
||||
if err == nil {
|
||||
err = sender.Send(jobCtx, participationEmail(p, s.appBaseURL, token, location))
|
||||
}
|
||||
cancel()
|
||||
// Persist even when sending consumed the job's deadline.
|
||||
saveCtx, saveCancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
saveErr := s.teamsRepo.FinishParticipationEmail(saveCtx, p.TeamID, p.Attempts, err)
|
||||
saveCancel()
|
||||
if saveErr != nil {
|
||||
return saveErr
|
||||
}
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package game_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"evening_detective_server/internal/modules/email_sender"
|
||||
"evening_detective_server/internal/repos/teams_repo"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
type participationSender struct {
|
||||
messages []email_sender.Message
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *participationSender) Send(_ context.Context, m email_sender.Message) error {
|
||||
s.messages = append(s.messages, m)
|
||||
return s.err
|
||||
}
|
||||
|
||||
func TestParticipationEmail(t *testing.T) {
|
||||
token := participationToken("test-secret", 42)
|
||||
if len(token) != 64 || token != participationToken("test-secret", 42) || token == participationToken("test-secret", 43) || token == participationToken("other", 42) {
|
||||
t.Fatal("token isolation or retry stability broken")
|
||||
}
|
||||
p := &teams_repo.Participation{TeamName: "<script>team</script>", GameName: "Game", Email: "captain@example.org", StartAt: time.Date(2026, 9, 20, 12, 0, 0, 0, time.UTC), ParticipantsCount: 5}
|
||||
m := participationEmail(p, "https://example.org", token, time.FixedZone("+07", 7*3600))
|
||||
if strings.Contains(m.HTML, "<script>") || !strings.Contains(m.HTML, "<script>") || !strings.Contains(m.Body, "19:00") || !strings.Contains(m.Body, "#token="+token) || !strings.Contains(m.HTML, "answer=cancelled") {
|
||||
t.Fatalf("invalid email: %+v", m)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParticipationPostgres(t *testing.T) {
|
||||
dsn := os.Getenv("TEST_DATABASE_URL")
|
||||
if dsn == "" {
|
||||
t.Skip("set TEST_DATABASE_URL to run PostgreSQL integration check")
|
||||
}
|
||||
ctx := context.Background()
|
||||
admin, err := pgxpool.New(ctx, dsn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer admin.Close()
|
||||
schema := fmt.Sprintf("participation_test_%d", time.Now().UnixNano())
|
||||
quoted := pgx.Identifier{schema}.Sanitize()
|
||||
if _, err = admin.Exec(ctx, "CREATE SCHEMA "+quoted); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if _, err := admin.Exec(ctx, "DROP SCHEMA "+quoted+" CASCADE"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}()
|
||||
config, err := pgxpool.ParseConfig(dsn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config.ConnConfig.RuntimeParams["search_path"] = schema
|
||||
config.ConnConfig.RuntimeParams["timezone"] = "UTC"
|
||||
pool, err := pgxpool.NewWithConfig(ctx, config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer pool.Close()
|
||||
exec := func(sql string, args ...any) {
|
||||
t.Helper()
|
||||
if _, err := pool.Exec(ctx, sql, args...); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
exec(`CREATE TABLE games(id SERIAL PRIMARY KEY,name TEXT DEFAULT 'Game',start_at TIMESTAMP NOT NULL,status TEXT DEFAULT 'draft');
|
||||
CREATE TABLE teams(id SERIAL PRIMARY KEY,name TEXT DEFAULT 'Team',creator_id INTEGER,game_id INTEGER REFERENCES games(id),password TEXT DEFAULT 'secret',status TEXT DEFAULT 'register',created_at TIMESTAMP DEFAULT (NOW() AT TIME ZONE 'UTC'));
|
||||
INSERT INTO games(start_at) VALUES((NOW() AT TIME ZONE 'UTC')+INTERVAL '12 hours'),((NOW() AT TIME ZONE 'UTC')+INTERVAL '2 days');
|
||||
INSERT INTO teams(game_id,created_at) VALUES(1,(NOW() AT TIME ZONE 'UTC')-INTERVAL '2 days'),(1,(NOW() AT TIME ZONE 'UTC')-INTERVAL '1 hour');`)
|
||||
for _, name := range []string{"20260913130000_add_team_registration.sql", "20260915120000_add_team_participation.sql"} {
|
||||
data, err := os.ReadFile("../../../migrations/" + name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
exec(strings.Split(string(data), "-- +goose Down")[0])
|
||||
}
|
||||
repo := teams_repo.NewTeamsRepo(pool)
|
||||
service := &GameService{teamsRepo: repo, appBaseURL: "https://example.org"}
|
||||
state := func(id int, want string) {
|
||||
t.Helper()
|
||||
team, err := repo.GetTeamByID(ctx, id)
|
||||
if err != nil || team.ParticipationStatus != want {
|
||||
t.Fatalf("team %d want %s got %+v, %v", id, want, team, err)
|
||||
}
|
||||
}
|
||||
state(1, "pending")
|
||||
state(2, "pending") // Existing teams are not changed by the migration.
|
||||
exec(`UPDATE teams SET captain_email='captain@example.org',participants_count=5;
|
||||
INSERT INTO teams(game_id,created_at,captain_email) SELECT 1,start_at-INTERVAL '24 hours','boundary@example.org' FROM games WHERE id=1;
|
||||
INSERT INTO teams(game_id,created_at,captain_email) SELECT 1,start_at-INTERVAL '24 hours'+INTERVAL '1 microsecond','late@example.org' FROM games WHERE id=1;
|
||||
INSERT INTO teams(game_id,captain_email) VALUES(2,'future@example.org');
|
||||
INSERT INTO teams(game_id,created_at) SELECT 1,start_at-INTERVAL '25 hours' FROM games WHERE id=1;`)
|
||||
state(3, "pending")
|
||||
state(4, "confirmed")
|
||||
state(5, "pending")
|
||||
// Independent workers must claim each due team just once.
|
||||
var wg sync.WaitGroup
|
||||
ids := make(chan int, 8)
|
||||
errs := make(chan error, 8)
|
||||
for range 8 {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
p, err := repo.ClaimParticipationEmail(ctx)
|
||||
if err == nil {
|
||||
ids <- p.TeamID
|
||||
} else if !errors.Is(err, pgx.ErrNoRows) {
|
||||
errs <- err
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
close(ids)
|
||||
close(errs)
|
||||
seen := map[int]bool{}
|
||||
for id := range ids {
|
||||
if seen[id] {
|
||||
t.Fatalf("duplicate claim %d", id)
|
||||
}
|
||||
seen[id] = true
|
||||
}
|
||||
for err := range errs {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(seen) != 2 || !seen[1] || !seen[3] {
|
||||
t.Fatalf("wrong eligible teams: %v", seen)
|
||||
}
|
||||
exec(`UPDATE teams SET participation_next_attempt_at=NULL`)
|
||||
sender := &participationSender{err: errors.New("SMTP unavailable")}
|
||||
if err := service.sendParticipationEmails(ctx, sender, "secret", time.UTC); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(sender.messages) != 2 {
|
||||
t.Fatalf("wanted two attempts: %d", len(sender.messages))
|
||||
}
|
||||
team, err := repo.GetTeamByID(ctx, 1)
|
||||
if err != nil || team.ParticipationEmailError == "" || team.ParticipationEmailSent {
|
||||
t.Fatalf("failed send not recorded: %+v %v", team, err)
|
||||
}
|
||||
sender.err = nil
|
||||
// Expired lease is recovered after a worker restart.
|
||||
exec(`UPDATE teams SET participation_next_attempt_at=NOW()-INTERVAL '1 minute' WHERE id=3`)
|
||||
if err := service.sendParticipationEmails(ctx, sender, "secret", time.UTC); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(sender.messages) != 4 {
|
||||
t.Fatal("expired lease not recovered")
|
||||
}
|
||||
exec(`UPDATE teams SET participation_attempts=8, participation_next_attempt_at=NULL, participation_sent_at=NULL WHERE id=1`)
|
||||
if err := service.sendParticipationEmails(ctx, sender, "secret", time.UTC); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(sender.messages) != 4 {
|
||||
t.Fatal("email retry exceeded the eight-attempt limit")
|
||||
}
|
||||
token := participationToken("secret", 1)
|
||||
if _, err := repo.GetParticipation(ctx, "bad"); !errors.Is(err, teams_repo.ErrParticipationLink) {
|
||||
t.Fatalf("invalid token: %v", err)
|
||||
}
|
||||
if _, err := repo.GetParticipation(ctx, strings.Repeat("0", 64)); !errors.Is(err, teams_repo.ErrParticipationLink) {
|
||||
t.Fatalf("unknown token: %v", err)
|
||||
}
|
||||
if _, err := repo.GetParticipation(ctx, token); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
state(1, "pending") // Viewing a link never answers.
|
||||
answers := make(chan error, 2)
|
||||
for _, answer := range []string{"confirmed", "cancelled"} {
|
||||
go func() { _, err := repo.AnswerParticipation(ctx, token, answer); answers <- err }()
|
||||
}
|
||||
success, conflict := 0, 0
|
||||
for range 2 {
|
||||
err := <-answers
|
||||
if err == nil {
|
||||
success++
|
||||
} else if errors.Is(err, teams_repo.ErrParticipationFinal) {
|
||||
conflict++
|
||||
} else {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if success != 1 || conflict != 1 {
|
||||
t.Fatalf("concurrent answers: success=%d conflict=%d", success, conflict)
|
||||
}
|
||||
p, err := repo.GetParticipation(ctx, token)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := repo.AnswerParticipation(ctx, token, p.Status); err != nil {
|
||||
t.Fatalf("same answer not idempotent: %v", err)
|
||||
}
|
||||
// Cancellation excludes teams from shared active listings and password-based gameplay.
|
||||
token3 := participationToken("secret", 3)
|
||||
if _, err := repo.AnswerParticipation(ctx, token3, "cancelled"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
active, err := repo.GetActiveTeamsByGameID(ctx, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, team := range active {
|
||||
if team.ID == 3 {
|
||||
t.Fatal("cancelled team in active list")
|
||||
}
|
||||
}
|
||||
if _, _, _, err := service.GetTeamActions(ctx, 3, "secret"); !errors.Is(err, teams_repo.ErrTeamNotFound) {
|
||||
t.Fatalf("cancelled team got story: %v", err)
|
||||
}
|
||||
if err := service.AddTeamAction(ctx, 3, "secret", "anything", false); !errors.Is(err, teams_repo.ErrTeamNotFound) {
|
||||
t.Fatalf("cancelled team added action: %v", err)
|
||||
}
|
||||
if _, err := service.GetTeamAnswers(ctx, 3, "secret"); !errors.Is(err, teams_repo.ErrTeamNotFound) {
|
||||
t.Fatalf("cancelled team got answers: %v", err)
|
||||
}
|
||||
token6 := participationToken("secret", 6)
|
||||
if err := repo.SetParticipationToken(ctx, 6, token6); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
exec(`UPDATE games SET status='run' WHERE id=1`)
|
||||
if _, err := repo.AnswerParticipation(ctx, token6, "confirmed"); !errors.Is(err, teams_repo.ErrParticipationClosed) {
|
||||
t.Fatalf("running game accepted answer: %v", err)
|
||||
}
|
||||
exec(`UPDATE games SET status='draft',start_at=(NOW() AT TIME ZONE 'UTC')-INTERVAL '1 second' WHERE id=1`)
|
||||
if _, err := repo.AnswerParticipation(ctx, token6, "confirmed"); !errors.Is(err, teams_repo.ErrParticipationClosed) {
|
||||
t.Fatalf("past scheduled start accepted answer: %v", err)
|
||||
}
|
||||
state(6, "pending")
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/mail"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
@@ -72,5 +73,10 @@ func (s *GameService) RegisterTeam(ctx context.Context, gameID int, deliveryID s
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return s.teamsRepo.RegisterTeam(ctx, gameID, deliveryID, registration.Name, registration.Email, registration.ParticipantsCount, password)
|
||||
game, err := s.gamesRepo.GetGameByID(ctx, gameID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
autoConfirm := time.Now().Before(game.StartAt.Add(-24*time.Hour)) == false
|
||||
return s.teamsRepo.RegisterTeamWithParticipation(ctx, gameID, deliveryID, registration.Name, registration.Email, registration.ParticipantsCount, password, autoConfirm)
|
||||
}
|
||||
|
||||
@@ -222,7 +222,11 @@ func (s *GameService) AddTeam(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, err := s.teamsRepo.AddTeam(ctx, creatorId, gameId, name, password)
|
||||
game, err := s.gamesRepo.GetGameByID(ctx, gameId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, err := s.teamsRepo.AddTeamWithParticipation(ctx, creatorId, gameId, name, password, !time.Now().Before(game.StartAt.Add(-24*time.Hour)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -250,7 +254,7 @@ func (s *GameService) GetTeamActions(ctx context.Context, teamId int, password s
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
if team.Password != password {
|
||||
if team.Password != password || team.ParticipationStatus == "cancelled" {
|
||||
return nil, nil, nil, teams_repo.ErrTeamNotFound
|
||||
}
|
||||
|
||||
@@ -287,7 +291,7 @@ func (s *GameService) AddTeamAction(ctx context.Context, teamId int, password st
|
||||
return err
|
||||
}
|
||||
|
||||
if team.Password != password {
|
||||
if team.Password != password || team.ParticipationStatus == "cancelled" {
|
||||
return teams_repo.ErrTeamNotFound
|
||||
}
|
||||
|
||||
@@ -386,7 +390,7 @@ func (s *GameService) getTeamAndGame(ctx context.Context, teamId int, password s
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if team.Password != password {
|
||||
if team.Password != password || team.ParticipationStatus == "cancelled" {
|
||||
return nil, nil, teams_repo.ErrTeamNotFound
|
||||
}
|
||||
game, err := s.gamesRepo.GetGameByID(ctx, team.GameId)
|
||||
@@ -494,7 +498,7 @@ func (s *GameService) GetGameAnswers(ctx context.Context, gameId int) (*GameAnsw
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
teams, err := s.teamsRepo.GetTeamsByGameID(ctx, gameId)
|
||||
teams, err := s.teamsRepo.GetActiveTeamsByGameID(ctx, gameId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -581,7 +585,7 @@ func (s *GameService) GetGameResults(ctx context.Context, gameId int) (*GameResu
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
teams, err := s.teamsRepo.GetTeamsByGameID(ctx, gameId)
|
||||
teams, err := s.teamsRepo.GetActiveTeamsByGameID(ctx, gameId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -3,12 +3,16 @@ package game_service
|
||||
import "evening_detective_server/internal/modules/storytelling"
|
||||
|
||||
type Team struct {
|
||||
CaptainEmail string
|
||||
ParticipantsCount int32
|
||||
ID int
|
||||
Name string
|
||||
Status string
|
||||
Password string
|
||||
ActionsCount int
|
||||
Applications []*storytelling.Application
|
||||
ParticipationStatus string
|
||||
ParticipationSource string
|
||||
ParticipationEmailError string
|
||||
ParticipationEmailSent bool
|
||||
CaptainEmail string
|
||||
ParticipantsCount int32
|
||||
ID int
|
||||
Name string
|
||||
Status string
|
||||
Password string
|
||||
ActionsCount int
|
||||
Applications []*storytelling.Application
|
||||
}
|
||||
|
||||
@@ -6,12 +6,16 @@ func mapTeam(
|
||||
o *repos.Team,
|
||||
) *Team {
|
||||
return &Team{
|
||||
ID: o.ID,
|
||||
CaptainEmail: o.CaptainEmail,
|
||||
ParticipantsCount: o.ParticipantsCount,
|
||||
Name: o.Name,
|
||||
Status: o.Status,
|
||||
Password: o.Password,
|
||||
ParticipationStatus: o.ParticipationStatus,
|
||||
ParticipationSource: o.ParticipationSource,
|
||||
ParticipationEmailError: o.ParticipationEmailError,
|
||||
ParticipationEmailSent: o.ParticipationEmailSent,
|
||||
ID: o.ID,
|
||||
CaptainEmail: o.CaptainEmail,
|
||||
ParticipantsCount: o.ParticipantsCount,
|
||||
Name: o.Name,
|
||||
Status: o.Status,
|
||||
Password: o.Password,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ func (s *GameService) DownloadTeamQR(ctx context.Context, id int) ([]byte, strin
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
teams, err := s.teamsRepo.GetTeamsByGameID(ctx, id)
|
||||
teams, err := s.teamsRepo.GetActiveTeamsByGameID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user