diff --git a/README.md b/README.md index c16a563..ff307e7 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ MCP-эндпоинт: `http://localhost:8090/api/mcp`. ВНИМАНИЕ: REST-ga `POST /api/games/{id}/registrations` — отдельный HTTP webhook, без пользовательского JWT. Капитан не должен иметь учётную запись. Создаётся обычная команда с автоматически -сгенерированным паролем; почта капитана и количество участников видны организатору +сгенерированным паролем; капитану отправляется письмо с данными регистрации, почта капитана и количество участников видны организатору в списке команд игры. Письма автоматически не отправляются. Перед запуском примените миграции и задайте `YANDEX_FORMS_TOKEN` в окружении сервера diff --git a/cmd/evening_detective_server/main.go b/cmd/evening_detective_server/main.go index 54b2e2d..00a6f7b 100644 --- a/cmd/evening_detective_server/main.go +++ b/cmd/evening_detective_server/main.go @@ -148,7 +148,7 @@ func main() { answerService := answer_service.NewAnswerService(gameRepo, teamRepo, scenarioService, answersRepo, actionsRepo) - teamService := team_service.NewTeamService(gameRepo, teamRepo, passwordGenerator, os.Getenv("APP_BASE_URL")) + teamService := team_service.NewTeamService(gameRepo, teamRepo, passwordGenerator, os.Getenv("APP_BASE_URL"), emailSender) participationService := participation_service.NewParticipationService(teamRepo, os.Getenv("APP_BASE_URL")) go participationService.RunParticipationEmails(context.Background(), emailSender, os.Getenv("PARTICIPATION_SECRET"), os.Getenv("GAME_TIMEZONE")) diff --git a/internal/modules/email_sender/template.go b/internal/modules/email_sender/template.go new file mode 100644 index 0000000..3932ba4 --- /dev/null +++ b/internal/modules/email_sender/template.go @@ -0,0 +1,44 @@ +package email_sender + +import "html" + +func ButtonHTML(link, label string) string { + return `
| +` + html.EscapeString(label) + ` + |
+
|
" + html.EscapeString(p.TeamName) + " — «" + html.EscapeString(p.GameName) + "»
" + html.EscapeString(date) + fmt.Sprintf(" · Участников: %d
", p.ParticipantsCount) + content := "Подтвердите участие в игре
" + html.EscapeString(p.TeamName) + " — «" + html.EscapeString(p.GameName) + "»
" + html.EscapeString(date) + fmt.Sprintf(" · Участников: %d
", p.ParticipantsCount) for _, action := range []struct{ value, label string }{{"confirmed", "Подтвердить участие"}, {"cancelled", "Отменить участие"}} { - markup += `` + content += email_sender.ButtonHTML(link+"&answer="+action.value, action.label) } - markup += "Ответ можно дать до начала игры. Решение окончательное: изменить его нельзя.
Без ответа запись сохраняется со статусом «Ожидает ответа».
Не пересылайте письмо: ссылка позволяет принять решение за команду.
" + content += "Ответ можно дать до начала игры. Решение окончательное: изменить его нельзя.
Без ответа запись сохраняется со статусом «Ожидает ответа».
Не пересылайте письмо: ссылка позволяет принять решение за команду.
" + markup := email_sender.WrapHTML(content, strings.TrimRight(baseURL, "/")) return email_sender.Message{To: p.Email, Subject: "Подтвердите участие в «Вечернем детективе»", Body: body, HTML: markup} } diff --git a/internal/services/team_service/registration_test.go b/internal/services/team_service/registration_test.go index b09b690..add1f2f 100644 --- a/internal/services/team_service/registration_test.go +++ b/internal/services/team_service/registration_test.go @@ -3,6 +3,8 @@ package team_service import ( "strings" "testing" + + "evening_detective_server/internal/repos" ) func TestRegistrationValidation(t *testing.T) { @@ -30,3 +32,24 @@ func TestRegistrationValidation(t *testing.T) { } } } + +func TestRegistrationEmail(t *testing.T) { + message := registrationEmail(&repos.Team{ + ID: 7, + Name: "Команда", + CaptainEmail: "captain@example.org", + ParticipantsCount: 5, + Password: "abcdefgh", + }, "Большая игра", "https://app.test") + if message.To != "captain@example.org" { + t.Errorf("wrong recipient: %q", message.To) + } + for _, value := range []string{message.Body, message.HTML} { + if !strings.Contains(value, "Команда") || !strings.Contains(value, "abcdefgh") { + t.Errorf("registration email misses team data: %q", value) + } + } + if !strings.Contains(message.Body, "https://app.test/team-story/7?password=abcdefgh") { + t.Error("registration email misses team link") + } +} diff --git a/internal/services/team_service/service.go b/internal/services/team_service/service.go index 91f5c41..fae39cb 100644 --- a/internal/services/team_service/service.go +++ b/internal/services/team_service/service.go @@ -3,13 +3,18 @@ package team_service import ( "context" "errors" + "fmt" + "html" + "strings" + "time" + + "evening_detective_server/internal/modules/email_sender" "evening_detective_server/internal/modules/game_model" "evening_detective_server/internal/modules/password_generator" "evening_detective_server/internal/modules/team_qr_pdf" + "evening_detective_server/internal/repos" "evening_detective_server/internal/repos/games_repo" "evening_detective_server/internal/repos/teams_repo" - "strings" - "time" ) type TeamService struct { @@ -17,6 +22,7 @@ type TeamService struct { teamsRepo *teams_repo.TeamsRepo passwordGenerator password_generator.IPasswordGenerator appBaseURL string + emailSender email_sender.IEmailSender } func NewTeamService( @@ -24,12 +30,14 @@ func NewTeamService( teamsRepo *teams_repo.TeamsRepo, passwordGenerator password_generator.IPasswordGenerator, appBaseURL string, + emailSender email_sender.IEmailSender, ) *TeamService { return &TeamService{ gamesRepo: gamesRepo, teamsRepo: teamsRepo, passwordGenerator: passwordGenerator, appBaseURL: appBaseURL, + emailSender: emailSender, } } @@ -86,7 +94,29 @@ func (s *TeamService) RegisterTeam(ctx context.Context, gameID int, deliveryID s 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) + id, err := s.teamsRepo.RegisterTeamWithParticipation(ctx, gameID, deliveryID, registration.Name, registration.Email, registration.ParticipantsCount, password, autoConfirm) + if err != nil { + return 0, err + } + if s.emailSender == nil { + return id, nil + } + team, err := s.teamsRepo.GetTeamByID(ctx, id) + if err != nil { + return 0, err + } + if err := s.emailSender.Send(ctx, registrationEmail(team, game.Name, s.appBaseURL)); err != nil { + return 0, fmt.Errorf("email send: %w", err) + } + return id, nil +} + +func registrationEmail(team *repos.Team, gameName, baseURL string) email_sender.Message { + link := strings.TrimRight(baseURL, "/") + "/team-story/" + fmt.Sprint(team.ID) + "?password=" + team.Password + body := fmt.Sprintf("Регистрация команды «%s» в игре «%s» прошла успешно.\n\nУчастников: %d\nПароль команды: %s\n\nСсылка для входа в игру: %s\n\nСохраните это письмо и не пересылайте пароль посторонним.", team.Name, gameName, team.ParticipantsCount, team.Password, link) + content := fmt.Sprintf("Регистрация команды %s в игре %s прошла успешно.
Участников: %d
Пароль команды: %s
Сохраните это письмо и не пересылайте пароль посторонним.
", html.EscapeString(team.Name), html.EscapeString(gameName), team.ParticipantsCount, html.EscapeString(team.Password), email_sender.ButtonHTML(link, "Войти в игру")) + markup := email_sender.WrapHTML(content, strings.TrimRight(baseURL, "/")) + return email_sender.Message{To: team.CaptainEmail, Subject: "Регистрация команды в «Вечернем детективе»", Body: body, HTML: markup} } func (s *TeamService) DownloadTeamQR(ctx context.Context, id int) ([]byte, string, error) {