diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 25c159d..79135e7 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -6,7 +6,7 @@ on: - main env: - VERSION: 1.1.3 + VERSION: 1.1.4 jobs: # ЧАСТЬ 1: Собираем образы и кладем в Gitea (чтобы делиться с ребятами) diff --git a/Directory.Build.props b/Directory.Build.props index d2af466..3d14da8 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 1.1.3 + 1.1.4 net10.0 preview enable diff --git a/compose.yaml b/compose.yaml index 39adb96..47b5f8c 100644 --- a/compose.yaml +++ b/compose.yaml @@ -18,7 +18,7 @@ services: retries: 10 bot: - image: git.codeanddice.ru/toutsu/gmrelay-bot:1.1.3 + image: git.codeanddice.ru/toutsu/gmrelay-bot:1.1.4 container_name: gmrelay_bot restart: always network_mode: host @@ -30,7 +30,7 @@ services: - "Telegram__BotToken=${TELEGRAM_BOT_TOKEN}" web: - image: git.codeanddice.ru/toutsu/gmrelay-web:1.1.3 + image: git.codeanddice.ru/toutsu/gmrelay-web:1.1.4 container_name: gmrelay_web restart: always network_mode: host diff --git a/src/GmRelay.Bot/Features/Sessions/CreateSession/CancelSessionHandler.cs b/src/GmRelay.Bot/Features/Sessions/CreateSession/CancelSessionHandler.cs index 6d51316..8e50505 100644 --- a/src/GmRelay.Bot/Features/Sessions/CreateSession/CancelSessionHandler.cs +++ b/src/GmRelay.Bot/Features/Sessions/CreateSession/CancelSessionHandler.cs @@ -48,7 +48,10 @@ public sealed class CancelSessionHandler( } // 2. Отменяем сессию - await connection.ExecuteAsync("UPDATE sessions SET status = 'Cancelled' WHERE id = @Id", new { Id = command.SessionId }, transaction); + await connection.ExecuteAsync( + "UPDATE sessions SET status = @Status WHERE id = @Id", + new { Id = command.SessionId, Status = SessionStatus.Cancelled }, + transaction); // 3. Загружаем весь батч для перерисовки var batchSessions = await connection.QueryAsync( diff --git a/src/GmRelay.Bot/Features/Sessions/CreateSession/CreateSessionHandler.cs b/src/GmRelay.Bot/Features/Sessions/CreateSession/CreateSessionHandler.cs index 17dda09..63f8db8 100644 --- a/src/GmRelay.Bot/Features/Sessions/CreateSession/CreateSessionHandler.cs +++ b/src/GmRelay.Bot/Features/Sessions/CreateSession/CreateSessionHandler.cs @@ -1,4 +1,5 @@ using Dapper; +using GmRelay.Shared.Domain; using GmRelay.Shared.Rendering; using Npgsql; using Telegram.Bot; @@ -93,7 +94,7 @@ public sealed class CreateSessionHandler( var sessionId = await connection.ExecuteScalarAsync( """ INSERT INTO sessions (batch_id, group_id, title, join_link, scheduled_at, status, thread_id) - VALUES (@BatchId, @GroupId, @Title, @Link, @ScheduledAt, 'Planned', @ThreadId) + VALUES (@BatchId, @GroupId, @Title, @Link, @ScheduledAt, @Status, @ThreadId) RETURNING id; """, new @@ -103,11 +104,12 @@ public sealed class CreateSessionHandler( Title = title, Link = link, ScheduledAt = scheduledAt, - ThreadId = messageThreadId + ThreadId = messageThreadId, + Status = SessionStatus.Planned }, transaction); - sessions.Add(new SessionBatchDto(sessionId, scheduledAt.UtcDateTime, "Planned")); + sessions.Add(new SessionBatchDto(sessionId, scheduledAt.UtcDateTime, SessionStatus.Planned)); } await transaction.CommitAsync(cancellationToken); diff --git a/src/GmRelay.Bot/Features/Sessions/ExportCalendar/ExportCalendarHandler.cs b/src/GmRelay.Bot/Features/Sessions/ExportCalendar/ExportCalendarHandler.cs index b9b9f50..809ab9d 100644 --- a/src/GmRelay.Bot/Features/Sessions/ExportCalendar/ExportCalendarHandler.cs +++ b/src/GmRelay.Bot/Features/Sessions/ExportCalendar/ExportCalendarHandler.cs @@ -1,5 +1,6 @@ using System.Text; using Dapper; +using GmRelay.Shared.Domain; using Npgsql; using Telegram.Bot; using Telegram.Bot.Types; @@ -21,10 +22,10 @@ public sealed class ExportCalendarHandler( FROM sessions s JOIN game_groups g ON s.group_id = g.id WHERE g.telegram_chat_id = @ChatId - AND s.status = 'Planned' + AND s.status = @Planned AND s.scheduled_at > NOW() ORDER BY s.scheduled_at ASC", - new { ChatId = message.Chat.Id }); + new { ChatId = message.Chat.Id, Planned = SessionStatus.Planned }); var sessionsList = sessions.ToList(); diff --git a/src/GmRelay.Bot/Features/Sessions/ListSessions/DeleteSessionHandler.cs b/src/GmRelay.Bot/Features/Sessions/ListSessions/DeleteSessionHandler.cs index 132f968..d48d057 100644 --- a/src/GmRelay.Bot/Features/Sessions/ListSessions/DeleteSessionHandler.cs +++ b/src/GmRelay.Bot/Features/Sessions/ListSessions/DeleteSessionHandler.cs @@ -80,10 +80,10 @@ public sealed class DeleteSessionHandler( FROM sessions s JOIN game_groups g ON s.group_id = g.id LEFT JOIN session_participants sp ON s.id = sp.session_id - WHERE g.telegram_chat_id = @ChatId AND s.status != 'Cancelled' AND s.scheduled_at > NOW() + WHERE g.telegram_chat_id = @ChatId AND s.status != @Cancelled AND s.scheduled_at > NOW() GROUP BY s.id, s.title, s.scheduled_at, s.status, g.gm_telegram_id ORDER BY s.scheduled_at ASC", - new { ChatId = command.ChatId }); + new { ChatId = command.ChatId, Cancelled = SessionStatus.Cancelled }); var sessionsList = sessions.ToList(); diff --git a/src/GmRelay.Bot/Features/Sessions/ListSessions/ListSessionsHandler.cs b/src/GmRelay.Bot/Features/Sessions/ListSessions/ListSessionsHandler.cs index 382c12a..a3b9bea 100644 --- a/src/GmRelay.Bot/Features/Sessions/ListSessions/ListSessionsHandler.cs +++ b/src/GmRelay.Bot/Features/Sessions/ListSessions/ListSessionsHandler.cs @@ -23,10 +23,10 @@ public sealed class ListSessionsHandler( FROM sessions s JOIN game_groups g ON s.group_id = g.id LEFT JOIN session_participants sp ON s.id = sp.session_id - WHERE g.telegram_chat_id = @ChatId AND s.status != 'Cancelled' AND s.scheduled_at > NOW() + WHERE g.telegram_chat_id = @ChatId AND s.status != @Cancelled AND s.scheduled_at > NOW() GROUP BY s.id, s.title, s.scheduled_at, s.status, g.gm_telegram_id ORDER BY s.scheduled_at ASC", - new { ChatId = message.Chat.Id }); + new { ChatId = message.Chat.Id, Cancelled = SessionStatus.Cancelled }); var sessionsList = sessions.ToList(); diff --git a/src/GmRelay.Bot/Features/Sessions/RescheduleSession/HandleRescheduleTimeInputHandler.cs b/src/GmRelay.Bot/Features/Sessions/RescheduleSession/HandleRescheduleTimeInputHandler.cs index 6a5fc0b..82498c4 100644 --- a/src/GmRelay.Bot/Features/Sessions/RescheduleSession/HandleRescheduleTimeInputHandler.cs +++ b/src/GmRelay.Bot/Features/Sessions/RescheduleSession/HandleRescheduleTimeInputHandler.cs @@ -154,10 +154,10 @@ public sealed class HandleRescheduleTimeInputHandler( await connection.ExecuteAsync( """ - UPDATE sessions SET scheduled_at = @NewTime, status = 'Planned', updated_at = now() + UPDATE sessions SET scheduled_at = @NewTime, status = @Status, updated_at = now() WHERE id = @SessionId """, - new { NewTime = newTime, proposal.SessionId }, + new { NewTime = newTime, proposal.SessionId, Status = SessionStatus.Planned }, transaction); await connection.ExecuteAsync( diff --git a/src/GmRelay.Bot/Features/Sessions/RescheduleSession/HandleRescheduleVoteHandler.cs b/src/GmRelay.Bot/Features/Sessions/RescheduleSession/HandleRescheduleVoteHandler.cs index 06429f6..07f0bd7 100644 --- a/src/GmRelay.Bot/Features/Sessions/RescheduleSession/HandleRescheduleVoteHandler.cs +++ b/src/GmRelay.Bot/Features/Sessions/RescheduleSession/HandleRescheduleVoteHandler.cs @@ -165,13 +165,13 @@ public sealed class HandleRescheduleVoteHandler( """ UPDATE sessions SET scheduled_at = @NewTime, - status = 'Planned', + status = @Status, confirmation_message_id = NULL, link_message_id = NULL, updated_at = now() WHERE id = @SessionId """, - new { NewTime = newTime, proposal.SessionId }, + new { NewTime = newTime, proposal.SessionId, Status = SessionStatus.Planned }, transaction); await connection.ExecuteAsync( diff --git a/src/GmRelay.Bot/Features/Sessions/RescheduleSession/InitiateRescheduleHandler.cs b/src/GmRelay.Bot/Features/Sessions/RescheduleSession/InitiateRescheduleHandler.cs index 2debb3d..4787959 100644 --- a/src/GmRelay.Bot/Features/Sessions/RescheduleSession/InitiateRescheduleHandler.cs +++ b/src/GmRelay.Bot/Features/Sessions/RescheduleSession/InitiateRescheduleHandler.cs @@ -1,4 +1,5 @@ using Dapper; +using GmRelay.Shared.Domain; using Npgsql; using Telegram.Bot; @@ -39,9 +40,9 @@ public sealed class InitiateRescheduleHandler( SELECT s.title AS Title, g.gm_telegram_id AS GmId FROM sessions s JOIN game_groups g ON s.group_id = g.id - WHERE s.id = @SessionId AND s.status != 'Cancelled' + WHERE s.id = @SessionId AND s.status != @Cancelled """, - new { command.SessionId }); + new { command.SessionId, Cancelled = SessionStatus.Cancelled }); if (session is null) { diff --git a/src/GmRelay.Shared/Domain/SessionStatus.cs b/src/GmRelay.Shared/Domain/SessionStatus.cs index a9e8238..8d30a90 100644 --- a/src/GmRelay.Shared/Domain/SessionStatus.cs +++ b/src/GmRelay.Shared/Domain/SessionStatus.cs @@ -1,3 +1,5 @@ +using System.Collections.Frozen; + namespace GmRelay.Shared.Domain; public static class SessionStatus @@ -6,4 +8,13 @@ public static class SessionStatus public const string ConfirmationSent = "ConfirmationSent"; public const string Confirmed = "Confirmed"; public const string Cancelled = "Cancelled"; + + public static IReadOnlySet All { get; } = + new[] { Planned, ConfirmationSent, Confirmed, Cancelled } + .ToFrozenSet(StringComparer.Ordinal); + + public static bool IsKnown(string status) => All.Contains(status); + + public static bool IsCancelled(string status) => + string.Equals(status, Cancelled, StringComparison.Ordinal); } diff --git a/src/GmRelay.Shared/Rendering/SessionBatchRenderer.cs b/src/GmRelay.Shared/Rendering/SessionBatchRenderer.cs index a4ba0ab..a83c036 100644 --- a/src/GmRelay.Shared/Rendering/SessionBatchRenderer.cs +++ b/src/GmRelay.Shared/Rendering/SessionBatchRenderer.cs @@ -36,14 +36,10 @@ public static class SessionBatchRenderer messageText += " Пока никто не записался\n"; } - if (session.Status == "Cancelled") + if (SessionStatus.IsCancelled(session.Status)) { messageText += "❌ Сессия отменена\n\n"; } - else if (session.Status == "RecruitmentClosed") - { - messageText += "🔒 Набор завершен\n\n"; - } else { messageText += "\n"; diff --git a/src/GmRelay.Web/Components/Pages/GroupDetails.razor b/src/GmRelay.Web/Components/Pages/GroupDetails.razor index b3e9f02..4de7ae1 100644 --- a/src/GmRelay.Web/Components/Pages/GroupDetails.razor +++ b/src/GmRelay.Web/Components/Pages/GroupDetails.razor @@ -134,15 +134,12 @@ SessionStatus.Confirmed => "status-success", SessionStatus.Cancelled => "status-danger", SessionStatus.ConfirmationSent => "status-warning", - "Recruiting" => "status-info", - "RecruitmentClosed" => "status-info", + SessionStatus.Planned => "status-info", _ => "status-neutral" }; private string TranslateStatus(string status) => status switch { - "Recruiting" => "Набор", - "RecruitmentClosed" => "Набор закрыт", SessionStatus.Planned => "Запланировано", SessionStatus.ConfirmationSent => "Ждём подтверждения", SessionStatus.Confirmed => "Подтверждено", diff --git a/tests/GmRelay.Bot.Tests/Domain/SessionStatusTests.cs b/tests/GmRelay.Bot.Tests/Domain/SessionStatusTests.cs new file mode 100644 index 0000000..d1773d6 --- /dev/null +++ b/tests/GmRelay.Bot.Tests/Domain/SessionStatusTests.cs @@ -0,0 +1,75 @@ +using System.Reflection; +using GmRelay.Shared.Domain; + +namespace GmRelay.Bot.Tests.Domain; + +public sealed class SessionStatusTests +{ + [Fact] + public void All_ShouldContainOnlyCanonicalSessionStatuses() + { + var allProperty = typeof(SessionStatus).GetProperty( + "All", + BindingFlags.Public | BindingFlags.Static); + + Assert.NotNull(allProperty); + + var allStatusValues = Assert.IsAssignableFrom>(allProperty.GetValue(null)); + var expectedStatusValues = new[] + { + SessionStatus.Planned, + SessionStatus.ConfirmationSent, + SessionStatus.Confirmed, + SessionStatus.Cancelled + } + .Order(StringComparer.Ordinal); + + Assert.Equal(expectedStatusValues, allStatusValues.Order(StringComparer.Ordinal)); + } + + [Fact] + public void ProductionSources_ShouldNotReferenceLegacySessionStatuses() + { + var repositoryRoot = FindRepositoryRoot(); + var productionFiles = Directory.EnumerateFiles(repositoryRoot, "*.*", SearchOption.AllDirectories) + .Where(path => IsProductionSource(path)) + .ToList(); + + var legacyStatuses = new[] { "Recruit" + "ing", "Recruitment" + "Closed" }; + var offenders = productionFiles + .SelectMany(path => legacyStatuses + .Where(status => File.ReadAllText(path).Contains(status, StringComparison.Ordinal)) + .Select(status => $"{Path.GetRelativePath(repositoryRoot, path)} contains {status}")) + .ToList(); + + Assert.Empty(offenders); + } + + private static bool IsProductionSource(string path) + { + var extension = Path.GetExtension(path); + var separator = Path.DirectorySeparatorChar; + + return path.Contains($"{separator}src{separator}", StringComparison.Ordinal) + && !path.Contains($"{separator}bin{separator}", StringComparison.Ordinal) + && !path.Contains($"{separator}obj{separator}", StringComparison.Ordinal) + && extension is ".cs" or ".razor" or ".sql"; + } + + private static string FindRepositoryRoot() + { + var current = new DirectoryInfo(AppContext.BaseDirectory); + + while (current is not null) + { + if (File.Exists(Path.Combine(current.FullName, "GM-Relay.slnx"))) + { + return current.FullName; + } + + current = current.Parent; + } + + throw new InvalidOperationException("Could not find repository root."); + } +} diff --git a/tests/GmRelay.Bot.Tests/Rendering/SessionBatchRendererTests.cs b/tests/GmRelay.Bot.Tests/Rendering/SessionBatchRendererTests.cs index 70bd87a..28f932e 100644 --- a/tests/GmRelay.Bot.Tests/Rendering/SessionBatchRendererTests.cs +++ b/tests/GmRelay.Bot.Tests/Rendering/SessionBatchRendererTests.cs @@ -6,7 +6,7 @@ namespace GmRelay.Bot.Tests.Rendering; public sealed class SessionBatchRendererTests { [Fact] - public void Render_ShouldOrderSessionsAndSkipButtonsForClosedStatuses() + public void Render_ShouldOrderSessionsAndSkipButtonsForCancelledSessions() { var firstSessionId = Guid.NewGuid(); var secondSessionId = Guid.NewGuid(); @@ -14,9 +14,9 @@ public sealed class SessionBatchRendererTests var sessions = new[] { - new SessionBatchDto(secondSessionId, new DateTime(2026, 4, 27, 18, 0, 0, DateTimeKind.Utc), "Planned"), - new SessionBatchDto(cancelledSessionId, new DateTime(2026, 4, 28, 18, 0, 0, DateTimeKind.Utc), "Cancelled"), - new SessionBatchDto(firstSessionId, new DateTime(2026, 4, 26, 18, 0, 0, DateTimeKind.Utc), "RecruitmentClosed") + new SessionBatchDto(secondSessionId, new DateTime(2026, 4, 27, 18, 0, 0, DateTimeKind.Utc), SessionStatus.Planned), + new SessionBatchDto(cancelledSessionId, new DateTime(2026, 4, 28, 18, 0, 0, DateTimeKind.Utc), SessionStatus.Cancelled), + new SessionBatchDto(firstSessionId, new DateTime(2026, 4, 26, 18, 0, 0, DateTimeKind.Utc), SessionStatus.Planned) }; var participants = new[] { @@ -37,9 +37,12 @@ public sealed class SessionBatchRendererTests Assert.True(secondIndex < thirdIndex); Assert.Contains("@alice", text); Assert.Contains("Bob", text); - Assert.Single(result.Markup.InlineKeyboard); + Assert.Equal(2, result.Markup.InlineKeyboard.Count()); Assert.Collection( buttons.Select(button => button.CallbackData), + callbackData => Assert.Equal($"join_session:{firstSessionId}", callbackData), + callbackData => Assert.Equal($"cancel_session:{firstSessionId}", callbackData), + callbackData => Assert.Equal($"reschedule_session:{firstSessionId}", callbackData), callbackData => Assert.Equal($"join_session:{secondSessionId}", callbackData), callbackData => Assert.Equal($"cancel_session:{secondSessionId}", callbackData), callbackData => Assert.Equal($"reschedule_session:{secondSessionId}", callbackData));