diff --git a/src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardSubmitter.cs b/src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardSubmitter.cs index ee613dc..3f281a1 100644 --- a/src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardSubmitter.cs +++ b/src/GmRelay.DiscordBot/Features/Sessions/Wizard/DiscordWizardSubmitter.cs @@ -12,7 +12,6 @@ using GmRelay.Shared.Platform; using Microsoft.Extensions.Logging; using NetCord; using NetCord.Rest; -using Npgsql; namespace GmRelay.DiscordBot.Features.Sessions.Wizard; @@ -32,25 +31,19 @@ public sealed class DiscordWizardSubmitter private readonly IWizardDraftRepository _drafts; private readonly IWizardContextStore _contextStore; private readonly ILogger _log; - private readonly IPlatformMessenger? _platformMessenger; - private readonly NpgsqlDataSource? _dataSource; public DiscordWizardSubmitter( CreateSessionHandler shared, RestClient rest, IWizardDraftRepository drafts, IWizardContextStore contextStore, - ILogger log, - IPlatformMessenger? platformMessenger = null, - NpgsqlDataSource? dataSource = null) + ILogger log) { _shared = shared; _rest = rest; _drafts = drafts; _contextStore = contextStore; _log = log; - _platformMessenger = platformMessenger; - _dataSource = dataSource; } /// @@ -91,6 +84,15 @@ public sealed class DiscordWizardSubmitter created.Add((cmd, result)); } + + // Success: replace the wizard message with a confirmation and + // clean up the draft so the user can start a new one later. + var confirmation = created.Count == 1 + ? $"✅ Создано: {created[0].Command.Title}" + : $"✅ Создано: {created[0].Command.Title} и ещё {created.Count - 1} сессия/сессии"; + await EditDraftMessageAsync(draft, confirmation, Array.Empty(), ct); + await _drafts.DeleteAsync(draft.Id, ct); + _contextStore.Remove(draft.Id); } catch (Exception ex) { diff --git a/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/GameCreationWizard.cs b/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/GameCreationWizard.cs index 215f207..5dadf70 100644 --- a/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/GameCreationWizard.cs +++ b/src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/GameCreationWizard.cs @@ -256,7 +256,7 @@ public sealed class GameCreationWizard case WizardStepNames.PoolSystemDuration when payload.DurationMinutes is null: return TryParseHours(input, out var pdur) - ? (WizardStepNames.Format, SetDurationMinutes(payload, pdur), payload) + ? (WizardStepNames.Capacity, SetDurationMinutes(payload, pdur), payload) : (null, "Неверная длительность (1..12 ч)", payload); case WizardStepNames.PoolSlotDateTime: @@ -421,7 +421,7 @@ public sealed class GameCreationWizard WizardStepNames.System => WizardStepNames.Cover, WizardStepNames.Duration => WizardStepNames.System, WizardStepNames.DateTime => WizardStepNames.Duration, - WizardStepNames.Capacity => WizardStepNames.DateTime, + WizardStepNames.Capacity => p.Type == WizardCreationType.Pool ? WizardStepNames.PoolSystemDuration : WizardStepNames.DateTime, WizardStepNames.Format => WizardStepNames.Capacity, WizardStepNames.Location => WizardStepNames.Format, WizardStepNames.Visibility => WizardStepNames.Location, diff --git a/tests/GmRelay.Bot.Tests/Discord/DiscordTimeParserTests.cs b/tests/GmRelay.Bot.Tests/Discord/DiscordTimeParserTests.cs new file mode 100644 index 0000000..930613b --- /dev/null +++ b/tests/GmRelay.Bot.Tests/Discord/DiscordTimeParserTests.cs @@ -0,0 +1,77 @@ +using GmRelay.DiscordBot.Features.Sessions; + +namespace GmRelay.Bot.Tests.Discord; + +public sealed class DiscordTimeParserTests +{ + [Fact] + public void ParseTimeInput_ShouldTreatInputAsMoscowTime() + { + var future = DateTimeOffset.UtcNow.AddDays(7); + var result = DiscordTimeParser.ParseTimeInput( + future.ToString("yyyy-MM-dd '15:00'", System.Globalization.CultureInfo.InvariantCulture)); + + Assert.True(result.IsSuccess); + // 15:00 MSK = 12:00 UTC + Assert.Equal(12, result.Value.Hour); + Assert.Equal(0, result.Value.Minute); + Assert.Equal(TimeSpan.Zero, result.Value.Offset); + } + + [Fact] + public void ParseTimeInput_ShouldParseDiscordDateFormat() + { + var expected = FutureDateAt1930(); + var result = DiscordTimeParser.ParseTimeInput( + expected.ToString("yyyy-MM-dd HH:mm", System.Globalization.CultureInfo.InvariantCulture)); + + Assert.True(result.IsSuccess); + Assert.Equal(expected.Year, result.Value.Year); + Assert.Equal(expected.Month, result.Value.Month); + Assert.Equal(expected.Day, result.Value.Day); + // Input is treated as Moscow time; 19:30 MSK = 16:30 UTC + Assert.Equal(16, result.Value.Hour); + Assert.Equal(30, result.Value.Minute); + } + + [Fact] + public void ParseTimeInput_ShouldRejectPastDate() + { + var result = DiscordTimeParser.ParseTimeInput("2020-01-01 00:00"); + Assert.False(result.IsSuccess); + } + + [Fact] + public void ParseTimeInput_ShouldParseRussianDateFormat() + { + var expected = FutureDateAt1930(); + var result = DiscordTimeParser.ParseTimeInput( + expected.ToString("dd.MM.yyyy HH:mm", System.Globalization.CultureInfo.InvariantCulture)); + + Assert.True(result.IsSuccess); + Assert.Equal(expected.Year, result.Value.Year); + Assert.Equal(expected.Month, result.Value.Month); + Assert.Equal(expected.Day, result.Value.Day); + } + + [Fact] + public void ParseTimeInput_ShouldRejectInvalidFormat() + { + var result = DiscordTimeParser.ParseTimeInput("not-a-date"); + Assert.False(result.IsSuccess); + Assert.NotNull(result.Error); + } + + private static DateTimeOffset FutureDateAt1930() + { + var future = DateTimeOffset.UtcNow.AddDays(7); + return new DateTimeOffset( + future.Year, + future.Month, + future.Day, + 19, + 30, + 0, + TimeSpan.Zero); + } +} diff --git a/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/GameCreationWizardStepTransitionsTests.cs b/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/GameCreationWizardStepTransitionsTests.cs index ea48e99..9ece600 100644 --- a/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/GameCreationWizardStepTransitionsTests.cs +++ b/tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/GameCreationWizardStepTransitionsTests.cs @@ -93,6 +93,48 @@ public sealed class GameCreationWizardStepTransitionsTests Assert.Equal(10, maxPlayers.GetInt32()); } + [Fact] + public async Task PoolSystemDuration_FreeTextDuration_AdvancesToCapacity() + { + var wizard = BuildWizard(out var drafts, out _); + var payload = new WizardPayload + { + Type = WizardCreationType.Pool, + Title = "Pool", + System = "Dnd5e", + }; + var draft = NewDraft(WizardStepNames.PoolSystemDuration, payload); + drafts.Seed(draft); + + await wizard.HandleInteractionAsync(TextInteraction("4", ownerId: draft.OwnerId), draft, CancellationToken.None); + + Assert.Equal(WizardStepNames.Capacity, draft.Step); + using var doc = JsonDocument.Parse(draft.PayloadJson); + var root = doc.RootElement; + Assert.True(root.TryGetProperty("durationMinutes", out var dur)); + Assert.Equal(240, dur.GetInt32()); + } + + [Fact] + public async Task Back_FromPoolCapacity_GoesToPoolSystemDuration() + { + var wizard = BuildWizard(out var drafts, out _); + var payload = new WizardPayload + { + Type = WizardCreationType.Pool, + Title = "Pool", + System = "Dnd5e", + DurationMinutes = 240, + }; + var draft = NewDraft(WizardStepNames.Capacity, payload); + drafts.Seed(draft); + + var data = WizardCallbackData.Back(); + await wizard.HandleInteractionAsync(CallbackInteraction(data, ownerId: draft.OwnerId), draft, CancellationToken.None); + + Assert.Equal(WizardStepNames.PoolSystemDuration, draft.Step); + } + [Theory] [InlineData("waitlist:on")] [InlineData("waitlist:off")]