generated from VLADIMIR/template
update
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
package participation_service
|
||||
|
||||
import "evening_detective_server/internal/repos/teams_repo"
|
||||
|
||||
func mapParticipation(p *teams_repo.Participation) *Participation {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
return &Participation{
|
||||
TeamID: p.TeamID,
|
||||
TeamName: p.TeamName,
|
||||
GameName: p.GameName,
|
||||
StartAt: p.StartAt,
|
||||
ParticipantsCount: p.ParticipantsCount,
|
||||
Status: p.Status,
|
||||
Closed: p.Closed,
|
||||
Email: p.Email,
|
||||
Attempts: p.Attempts,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package participation_service
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"evening_detective_server/internal/repos/teams_repo"
|
||||
)
|
||||
|
||||
// Participation — данные участия, доступные транспортному и email-сценариям.
|
||||
type Participation struct {
|
||||
TeamID int
|
||||
TeamName string
|
||||
GameName string
|
||||
StartAt time.Time
|
||||
ParticipantsCount int32
|
||||
Status string
|
||||
Closed bool
|
||||
Email string
|
||||
Attempts int
|
||||
}
|
||||
|
||||
var (
|
||||
ErrParticipationLink = teams_repo.ErrParticipationLink
|
||||
ErrParticipationClosed = teams_repo.ErrParticipationClosed
|
||||
ErrParticipationFinal = teams_repo.ErrParticipationFinal
|
||||
)
|
||||
@@ -33,7 +33,7 @@ func TestParticipationEmail(t *testing.T) {
|
||||
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}
|
||||
p := &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)
|
||||
@@ -174,10 +174,10 @@ func TestParticipationPostgres(t *testing.T) {
|
||||
t.Fatal("email retry exceeded the eight-attempt limit")
|
||||
}
|
||||
token := participationToken("secret", 1)
|
||||
if _, err := service.GetParticipation(ctx, "bad"); !errors.Is(err, teams_repo.ErrParticipationLink) {
|
||||
if _, err := service.GetParticipation(ctx, "bad"); !errors.Is(err, ErrParticipationLink) {
|
||||
t.Fatalf("invalid token: %v", err)
|
||||
}
|
||||
if _, err := service.GetParticipation(ctx, strings.Repeat("0", 64)); !errors.Is(err, teams_repo.ErrParticipationLink) {
|
||||
if _, err := service.GetParticipation(ctx, strings.Repeat("0", 64)); !errors.Is(err, ErrParticipationLink) {
|
||||
t.Fatalf("unknown token: %v", err)
|
||||
}
|
||||
if _, err := service.GetParticipation(ctx, token); err != nil {
|
||||
@@ -193,7 +193,7 @@ func TestParticipationPostgres(t *testing.T) {
|
||||
err := <-answers
|
||||
if err == nil {
|
||||
success++
|
||||
} else if errors.Is(err, teams_repo.ErrParticipationFinal) {
|
||||
} else if errors.Is(err, ErrParticipationFinal) {
|
||||
conflict++
|
||||
} else {
|
||||
t.Fatal(err)
|
||||
@@ -239,11 +239,11 @@ func TestParticipationPostgres(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
exec(`UPDATE games SET status='run' WHERE id=1`)
|
||||
if _, err := service.AnswerParticipation(ctx, token6, "confirmed"); !errors.Is(err, teams_repo.ErrParticipationClosed) {
|
||||
if _, err := service.AnswerParticipation(ctx, token6, "confirmed"); !errors.Is(err, 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 := service.AnswerParticipation(ctx, token6, "confirmed"); !errors.Is(err, teams_repo.ErrParticipationClosed) {
|
||||
if _, err := service.AnswerParticipation(ctx, token6, "confirmed"); !errors.Is(err, ErrParticipationClosed) {
|
||||
t.Fatalf("past scheduled start accepted answer: %v", err)
|
||||
}
|
||||
state(6, "pending")
|
||||
|
||||
@@ -28,11 +28,13 @@ func NewParticipationService(teamsRepo *teams_repo.TeamsRepo, appBaseURL string)
|
||||
return &ParticipationService{teamsRepo: teamsRepo, appBaseURL: appBaseURL}
|
||||
}
|
||||
|
||||
func (s *ParticipationService) GetParticipation(ctx context.Context, token string) (*teams_repo.Participation, error) {
|
||||
return s.teamsRepo.GetParticipation(ctx, token)
|
||||
func (s *ParticipationService) GetParticipation(ctx context.Context, token string) (*Participation, error) {
|
||||
p, err := s.teamsRepo.GetParticipation(ctx, token)
|
||||
return mapParticipation(p), err
|
||||
}
|
||||
func (s *ParticipationService) AnswerParticipation(ctx context.Context, token, answer string) (*teams_repo.Participation, error) {
|
||||
return s.teamsRepo.AnswerParticipation(ctx, token, answer)
|
||||
func (s *ParticipationService) AnswerParticipation(ctx context.Context, token, answer string) (*Participation, error) {
|
||||
p, err := s.teamsRepo.AnswerParticipation(ctx, token, answer)
|
||||
return mapParticipation(p), err
|
||||
}
|
||||
|
||||
// Стабильные токены для разных целей сохраняют работоспособность ссылок при
|
||||
@@ -45,7 +47,7 @@ func participationToken(secret string, id int) string {
|
||||
return hex.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
func participationEmail(p *teams_repo.Participation, baseURL, token string, location *time.Location) email_sender.Message {
|
||||
func participationEmail(p *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)
|
||||
@@ -89,7 +91,7 @@ func (s *ParticipationService) RunParticipationEmails(ctx context.Context, sende
|
||||
func (s *ParticipationService) 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)
|
||||
repoParticipation, err := s.teamsRepo.ClaimParticipationEmail(jobCtx)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
cancel()
|
||||
return nil
|
||||
@@ -98,6 +100,7 @@ func (s *ParticipationService) sendParticipationEmails(ctx context.Context, send
|
||||
cancel()
|
||||
return err
|
||||
}
|
||||
p := mapParticipation(repoParticipation)
|
||||
token := participationToken(secret, p.TeamID)
|
||||
err = s.teamsRepo.SetParticipationToken(jobCtx, p.TeamID, token)
|
||||
if err == nil {
|
||||
|
||||
Reference in New Issue
Block a user