# Issue #112 — Wizard platform-neutral refactor ## Summary Moved the game-creation wizard's state machine and view builder from `GmRelay.Bot` to `GmRelay.Shared`, replacing the Telegram-typed `ITelegramWizardMessenger` / `Update` / `Message` surface with a platform-neutral `IWizardMessenger` / `WizardInteraction` contract. Telegram continues to work unchanged through a new `TelegramWizardMessenger` adapter and `WizardInteractionMapper`. The wizard core is now ready for a future Discord adapter without touching the state machine, and a `platform` column on `wizard_drafts` discriminates drafts from different messengers. ## Branch `feat/issue-112-wizard-refactor` (off `main`). ## Changed files ### New (Shared — platform-neutral wizard core) - `src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/IWizardMessenger.cs` — `IWizardMessenger` (Edit/Send/Answer/GetOwnerClubs), `WizardAction` + `WizardActionStyle`, `WizardKeyboard`, `WizardClubOption`, `WizardInteraction`, `IWizardDraftRepository` (now takes `platform, ownerId`). - `src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardStepLimits.cs` — `MaxTitleLength`, `MaxDescriptionLength`, `MaxSystemLength`, capacity and duration bounds (moved out of the Bot-only `WizardStep`). - `src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardStepNames.cs` — moved from Bot (unchanged content). - `src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardCallbackData.cs` — moved from Bot (unchanged content; `wizard:cancel` / `wizard:back` / `wizard:choice:step:value` format preserved for back-compat). - `src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardStorageException.cs` — moved from Bot (unchanged content). - `src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardStepViewBuilder.cs` — new platform-neutral view builder; produces `(string Text, IReadOnlyList Actions)` for each step. Replaces the Telegram-only `WizardStep.Render(text, InlineKeyboardMarkup)`. - `src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/GameCreationWizard.cs` — moved from Bot; rewritten to take `IWizardMessenger` + `IWizardDraftRepository` and a `WizardInteraction` (no more `Update`/`Message`/`CallbackQuery`). ### Updated (Shared) - `src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardDraft.cs` — `ChatId`, `MessageThreadId`, `OwnerId`, `DraftMessageId` switched to `string?` to fit both Telegram and Discord ids; new `Platform` field (defaults to `"Telegram"` for backward compatibility with pre-V032 rows). - `src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/WizardDraftRepository.cs` — `GetActiveAsync(platform, ownerId)`; selects/inserts the new `platform` column and the renamed `owner_id`. - (Deleted) `src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/IWizardDraftRepository.cs` — merged into `IWizardMessenger.cs` to keep wizard contracts colocated. ### New (Bot — Telegram adapter) - `src/GmRelay.Bot/Features/Sessions/CreateSession/Wizard/WizardInteractionMapper.cs` — converts `Telegram.Bot.Types.Update` → `WizardInteraction`. The single bridge between Telegram's update type and the platform-neutral wizard. - `src/GmRelay.Bot/Features/Sessions/CreateSession/Wizard/TelegramWizardMessenger.cs` — rewritten to implement `IWizardMessenger`; serialises `(WizardDraft, text, IReadOnlyList)` to Telegram `EditMessageText` / `SendMessage` / `AnswerCallbackQuery`. Club lookup SQL unchanged. ### Updated (Bot) - `src/GmRelay.Bot/Features/Sessions/CreateSession/Wizard/WizardStep.cs` — kept as the Telegram-side keyboard renderer. Re-exports the `WizardStepLimits` constants (so legacy call sites still work) and now delegates to `WizardStepViewBuilder.Build(...)` for the (text, actions) pair, then converts actions to `InlineKeyboardMarkup`. - `src/GmRelay.Bot/Features/Sessions/CreateSession/CreateSessionHandler.cs` — depends on `IWizardMessenger`; `StartWizardAsync` / `SubmitDraftAsync` use the new messenger contract; sets `Platform = "Telegram"` on the draft; uses string ids for `ChatId` / `MessageThreadId` / `OwnerId`. - `src/GmRelay.Bot/Infrastructure/Telegram/UpdateRouter.cs` — takes the Shared `GameCreationWizard`; uses `WizardInteractionMapper.TryMap` to feed the wizard with a platform-neutral `WizardInteraction`. Draft lookup uses `(platform, ownerId)`. - `src/GmRelay.Bot/Program.cs` — DI: `IWizardMessenger` → `TelegramWizardMessenger`; `GameCreationWizard` resolved from Shared. - (Deleted) `src/GmRelay.Bot/Features/Sessions/CreateSession/Wizard/GameCreationWizard.cs` - (Deleted) `src/GmRelay.Bot/Features/Sessions/CreateSession/Wizard/WizardStepNames.cs` - (Deleted) `src/GmRelay.Bot/Features/Sessions/CreateSession/Wizard/WizardCallbackData.cs` - (Deleted) `src/GmRelay.Bot/Features/Sessions/CreateSession/Wizard/WizardStorageException.cs` - (Deleted) `src/GmRelay.Bot/Features/Sessions/CreateSession/Wizard/ITelegramWizardMessenger.cs` ### New (Bot migrations) - `src/GmRelay.Bot/Migrations/V032__wizard_drafts_platform.sql` — adds `platform TEXT NOT NULL DEFAULT 'Telegram'`; converts `chat_id`, `message_thread_id`, `draft_message_id` from numeric types to `TEXT`; renames `owner_telegram_id` → `owner_id` and converts to `TEXT`; rebuilds the owner lookup index to use `(platform, owner_id)`. ### Tests - `tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardTestFakes.cs` — `FakeWizardMessenger` now implements `IWizardMessenger`; `NewDraft` sets `Platform = "Telegram"` and uses string ids; new helper factories `CallbackInteraction`, `TextInteraction`, `PhotoInteraction` build the platform-neutral `WizardInteraction`; legacy `CallbackUpdate` / `TextUpdate` helpers preserved for router-level tests that still consume `Update`. - `tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardDraftRepositoryFixture.cs` — schema updated to TEXT columns + `platform` column. - `tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardDraftRepositoryTests.cs` — uses the new `GetActiveAsync(platform, ownerId)` signature and string ids. - All wizard / create-session test files updated to call `wizard.HandleInteractionAsync(...)` with a `WizardInteraction` instead of `wizard.HandleUpdateAsync(Update, …)`. - `tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardInteractionMapperTests.cs` (new) — 5 cases covering callback / text / photo / captioned-photo / empty updates. - `tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/Wizard/WizardStepRenderTests.cs` — `NewDraft` updated to use string `ChatId`. ## Verification - `dotnet build` — full solution builds with 0 warnings, 0 errors. - `dotnet test tests/GmRelay.Bot.Tests/GmRelay.Bot.Tests.csproj` — 568/571 pass (2 pre-existing `[Fact(Skip = …)]` happy-path tests skipped, 1 pre-existing test failure in `CampaignTemplatesNavigationTests.NavMenu_ShouldExposeCurrentProjectVersion` which expects the old `v3.7.1` string in `NavMenu.razor` after the 3.8.0 release bump in commit `71080ae` — unrelated to this refactor). All 101 wizard / create-session tests pass. - `dotnet format --verify-no-changes` — clean. - `git grep "Telegram.Bot" src/GmRelay.Shared/` — empty. - `git grep "NetCord" src/GmRelay.Bot/` — empty. - `GameCreationWizard.cs` exists exactly once, in `src/GmRelay.Shared/Features/Sessions/CreateSession/Wizard/`. - `IWizardMessenger` lives in `src/GmRelay.Shared/...`; `ITelegramWizardMessenger` is gone. - `dotnet list package --vulnerable` — clean. ## Notes for the verifier - The callback-data wire format (`wizard:cancel`, `wizard:back`, `wizard:create`, `wizard:choice:{step}:{value}`) is unchanged; the router-level `wizard:resume` / `wizard:reset` controls are still produced by `UpdateRouter`. - `WizardDraft.DraftMessageId` switched from `long?` to `string?` so the same column can hold Discord's 64-bit snowflakes. V032 converts `BIGINT → TEXT`; existing rows in the V031 schema (if any are still live) survive the cast. - The `FakeWizardMessenger` keeps the long-typed recording shape (`Edits` / `Sends` tuples) so the existing assertions in `CreateSessionHandlerSubmitMissingFieldsTests` etc. keep working without rewrites. The fake converts `draft.ChatId` / `draft.MessageThreadId` to long on the way out, matching the old test contract. - The Telegram-renderer Bot-side `WizardStep` keeps the same public surface (`WizardStep.Render(draft, payload, clubs)` returning `(string, InlineKeyboardMarkup)`) so call sites in `UpdateRouter.TryHandleDraftControlCallbackAsync` and the `CreateSessionHandler` continue to work. The view layer behind it is now `WizardStepViewBuilder`. - A future `DiscordWizardMessenger` and `DiscordWizardInteractionMapper` can be added without any change to the Shared wizard; this is deliberately not part of this task (the plan calls it out as Task 2).