refactor(#22): разделить SessionBatchRenderer на neutral view и Telegram renderer

- SessionBatchViewBuilder в Shared собирает нейтральную view model
- TelegramSessionBatchRenderer в Bot/Web рендерит HTML + InlineKeyboardMarkup
- DiscordSessionBatchRenderer заглушка подготовлена
- BatchMessageEditor перенесён из Shared в Bot/Web
- Удалён SessionBatchRenderer, убран Telegram.Bot из Shared.csproj
- Обновлены все вызовы (7 handler-ов + Web SessionService + smoke tests)
- Новые тесты на builder и Telegram renderer
This commit is contained in:
root
2026-05-06 07:57:23 +00:00
parent 5dee2d87f5
commit 14b9bf15f2
25 changed files with 741 additions and 157 deletions
+250
View File
@@ -0,0 +1,250 @@
# Issue #19: Выровнять /newsession с batch-сценарием лендинга
> **Goal:** Сделать `/newsession` атомарным: либо весь batch создаётся целиком, либо ничего — с понятным сообщением об ошибках. Убрать создание частичных сессий при наличии любых ошибок ввода.
**Architecture:** Изменить `NewSessionParseResult.IsValid` так, чтобы он учитывал ЛЮБЫЕ parse-ошибки (некорректные даты, лимиты, повторы, прошедшие даты). Обновить `CreateSessionHandler`, чтобы при `!IsValid` отправлялось одно детальное сообщение с перечислением ошибок и help-шаблоном, и creation прерывался до транзакции БД.
**Tech Stack:** C#, .NET, Dapper, Telegram.Bot, xUnit
---
### Task 1: Добавить `HasErrors` и ужесточить `IsValid`
**Objective:** Запретить частичное создание: `IsValid == false`, если есть хоть одна ошибка парсинга.
**Files:**
- Modify: `src/GmRelay.Bot/Features/Sessions/CreateSession/NewSessionCommandParser.cs`
- Test: `tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/NewSessionCommandParserTests.cs`
**Step 1: RED — написать failing test**
Добавить в конец `NewSessionCommandParserTests.cs`:
```csharp
[Fact]
public void Parse_ShouldBeInvalid_WhenAnyErrorsPresent()
{
var nowUtc = new DateTimeOffset(2026, 4, 23, 12, 0, 0, TimeSpan.Zero);
var text = """
Название: Delta Green
Время: 25.04.2026 19:30
Время: 31.04.2026 19:30
Ссылка: https://example.test/dg
""";
var result = NewSessionCommandParser.Parse(text, nowUtc);
Assert.True(result.HasErrors);
Assert.False(result.IsValid);
}
```
**Step 2: Run test and verify RED**
```bash
cd /repo && dotnet test tests/GmRelay.Bot.Tests --filter "FullyQualifiedName~NewSessionCommandParserTests" -v n
```
Expected: `HasErrors` property not found → compile error or FAIL.
**Step 3: GREEN — minimal implementation**
В `NewSessionCommandParser.cs`, в `NewSessionParseResult`, заменить `IsValid` на:
```csharp
public bool HasErrors =>
PastTimeInputs.Count > 0 ||
InvalidTimeInputs.Count > 0 ||
InvalidSeatLimitInputs.Count > 0 ||
InvalidRecurringInputs.Count > 0;
public bool IsValid =>
!string.IsNullOrWhiteSpace(Title) &&
!string.IsNullOrWhiteSpace(Link) &&
ScheduledTimes.Count > 0 &&
!HasErrors;
```
**Step 4: Run test and verify GREEN**
```bash
cd /repo && dotnet test tests/GmRelay.Bot.Tests --filter "FullyQualifiedName~NewSessionCommandParserTests" -v n
```
Expected: 7 passed (включая новый).
**Step 5: Commit**
```bash
git add src/GmRelay.Bot/Features/Sessions/CreateSession/NewSessionCommandParser.cs
git add tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/NewSessionCommandParserTests.cs
git commit -m "feat(#19): add HasErrors to prevent partial batch creation"
```
---
### Task 2: Обновить существующий тест на Past/Invalid times
**Objective:** Старый тест ожидал `IsValid == true` при partial invalid — теперь это баг, нужно обновить assertion.
**Files:**
- Modify: `tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/NewSessionCommandParserTests.cs:114`
**Step 1: Write failing expectation first**
Заменить в методе `Parse_ShouldCollectPastAndInvalidTimes` (примерно строка 114):
```csharp
// Было:
Assert.True(result.IsValid);
// Стало:
Assert.False(result.IsValid);
```
**Step 2: Run test and verify RED**
```bash
cd /repo && dotnet test tests/GmRelay.Bot.Tests --filter "Parse_ShouldCollectPastAndInvalidTimes" -v n
```
Expected: FAIL — expected False, actual True.
**Step 3: Fix already done in Task 1**
Task 1 уже изменил `IsValid`. Перезапустить тест.
**Step 4: Run test and verify GREEN**
```bash
cd /repo && dotnet test tests/GmRelay.Bot.Tests --filter "Parse_ShouldCollectPastAndInvalidTimes" -v n
```
Expected: PASS.
**Step 5: Commit**
```bash
git add tests/GmRelay.Bot.Tests/Features/Sessions/CreateSession/NewSessionCommandParserTests.cs
git commit -m "test(#19): update assertion - partial invalid means invalid"
```
---
### Task 3: Обновить `CreateSessionHandler` для атомарной валидации и единого сообщения об ошибках
**Objective:** Убрать разбросанные warning-сообщения. При любых ошибках — одно сообщение с перечнем ошибок + help-шаблон. Не создавать сессии.
**Files:**
- Modify: `src/GmRelay.Bot/Features/Sessions/CreateSession/CreateSessionHandler.cs`
**Step 1: RED — напишем интеграционный поведенческий тест (опционально, landing test уже покрывает happy path)**
Просто запустим landing promise smoke test и убедимся, что он ещё green (happy path не сломан).
```bash
cd /repo && dotnet test tests/GmRelay.Bot.Tests --filter "TelegramLandingPromisesSmokeTests" -v n
```
Expected: PASS.
**Step 2: GREEN — minimal change in handler**
В `CreateSessionHandler.cs`, метод `HandleAsync`, заменить начало (строки 19–59) на:
```csharp
var parseResult = NewSessionCommandParser.Parse(message.Text ?? message.Caption, DateTimeOffset.UtcNow);
var errorMessages = new List<string>();
foreach (var timeInput in parseResult.PastTimeInputs)
errorMessages.Add($"⚠️ Дата {timeInput} находится в прошлом и будет пропущена.");
foreach (var timeInput in parseResult.InvalidTimeInputs)
errorMessages.Add($"⚠️ Некорректный формат времени '{timeInput}'. Пропущено.");
foreach (var seatLimitInput in parseResult.InvalidSeatLimitInputs)
errorMessages.Add($"⚠️ Некорректный лимит мест '{seatLimitInput}'. Укажите целое число больше 0.");
foreach (var recurringInput in parseResult.InvalidRecurringInputs)
errorMessages.Add($"⚠️ Некорректный повтор расписания '{recurringInput}'. Укажите число игр 1–52 и шаг 1–365 дней.");
if (!parseResult.IsValid)
{
var helpText = """
Не удалось распознать формат. Пожалуйста, используйте шаблон:
/newsession
Название: My Game
Время: 15.05.2026 19:30
Время: 22.05.2026 19:30
Мест: 4
Ссылка: https://link
Картинка: https://cover
Для повтора можно указать одну дату и строки:
Игр: 4
Интервал: 7
""";
if (errorMessages.Count > 0)
{
helpText = string.Join('\n', errorMessages) + "\n\n" + helpText;
}
await botClient.SendMessage(
chatId: message.Chat.Id,
text: helpText,
cancellationToken: cancellationToken);
return;
}
```
**Step 3: Build and verify**
```bash
cd /repo && dotnet build src/GmRelay.Bot
```
Expected: Build succeeded.
**Step 4: Run all relevant tests**
```bash
cd /repo && dotnet test tests/GmRelay.Bot.Tests --filter "FullyQualifiedName~CreateSession|FullyQualifiedName~Landing" -v n
```
Expected: All passed.
**Step 5: Commit**
```bash
git add src/GmRelay.Bot/Features/Sessions/CreateSession/CreateSessionHandler.cs
git commit -m "feat(#19): atomic validation with detailed error message in /newsession"
```
---
### Task 4: Проверить полный test suite на регрессии
**Objective:** Убедиться, что изменения не сломали существующие тесты.
**Files:**
- Test: все тесты проекта `GmRelay.Bot.Tests`
**Step 1: Run full test suite**
```bash
cd /repo && dotnet test tests/GmRelay.Bot.Tests -v n
```
Expected: All passed.
**Step 2: Commit (if any test baseline updated)**
Если всё зелёное — commit message:
```bash
git commit --allow-empty -m "test(#19): verify full suite green after atomic validation"
```
---
## Verification Checklist
- [ ] `NewSessionCommandParser.Parse` возвращает `IsValid = false` при любых ошибках (past/invalid times, seat limits, recurring).
- [ ] `HasErrors` true ↔ есть хотя бы одна ошибка.
- [ ] `CreateSessionHandler` не открывает БД-транзакцию, если `!IsValid`.
- [ ] Пользователь получает одно сообщение с перечислением ошибок + help.
- [ ] Landing promise smoke test проходит (happy path не сломан).
- [ ] Полный test suite зелёный.