# Спринт 6 — технический долг + 2FA

Автономная работа. После каждого пункта: `dotnet build` (SDK 8.0.126),
unit + integration тесты, коммит порцией, отметка `[x]`, коммит прогресса.

Не ломать auth. НЕ трогать global.json/POS/nginx/ОФД.

## Чек-лист

1. [x] **TD-6 Concurrency-токены на документах** — `RowVersion` (PostgreSQL
   `xmin` через `IsRowVersion`) на Supply/Demand/RetailSale/Transfer/Inventory.
   Миграция. EF concurrency check. 409 при конфликте. Тест: два параллельных
   PUT → один 200, другой 409.
   ✅ Postgres `xmin` (system column, без миграции) через
   `UseXminAsConcurrencyToken` для 5 документов + `IVersionedEntity`-marker.
   SuppliesController PUT принимает `Xmin`, сверяет, 409 при mismatch.
   Bonus: Supply.Update перешёл на ExecuteDelete+AddRange (тот же fix что
   в RetailSale). 2 интеграционных теста.
2. [x] **TD-2 FluentValidation** — `FluentValidation.AspNetCore`, validator'ы
   для SupplyInputDto/RetailSaleInputDto/ProductInputDto/CounterpartyInputDto/
   EmployeeInputDto. Auto-register. Тесты на каждый.
   ✅ Не используем deprecated `FluentValidation.AspNetCore` — текущая
   рекомендация: `AddValidatorsFromAssemblyContaining<Program>()` + кастомный
   `ValidationFilter` (IAsyncActionFilter). На неуспех → 400 ValidationProblemDetails.
   5 validator'ов, 16 unit-тестов, 70 integration зелёных без регрессий.
3. [x] **TD-4 Структурные log-fields в Serilog** — `LogContext.PushProperty`
   в middleware: OrgId, UserId, CorrelationId. Бизнес-логи (Supply.Post,
   Sale.Post) — структурно. `docs/logging.md`.
   ✅ `LogEnrichmentMiddleware` после Authentication. CorrelationId из заголовка
   `X-Correlation-ID` или генерируется. Business-логи на Supply.Post /
   RetailSale.Post с именованными плейсхолдерами. `docs/logging.md`
   с паттерном + анти-паттернами (string interpolation, PII в логах).
4. [x] **TD-1 CQRS partial (MediatR)** — `CreateSupplyCommand`,
   `PostRetailSaleCommand`, `GetSalesReportQuery`. Показать паттерн, не
   полный рефакторинг. Тесты на handlers.
   ✅ MediatR подключён в `food-market.api` с авторегистрацией из
   `food-market.application`. 3 handler-образца с абстракциями
   (`ISupplyWriter`, `IRetailSalePoster`) — testable без EF/БД.
   Контроллеры остались на прежнем flow (поэтапная миграция). 6 unit-тестов.
5. [x] **P2-4 2FA для админов (TOTP)** — `AuthenticatorTokenProvider`,
   endpoints `/api/me/2fa/enroll`, `/api/me/2fa/verify`, `/api/me/2fa/disable`.
   Опционально для Admin+SuperAdmin. При логине с включённым 2FA — два шага.
   ✅ Identity `AuthenticatorTokenProvider` (RFC 6238). Endpoints:
   /api/me/2fa/{status, enroll, verify, disable} с QR-URI. Password-grant
   на `/connect/token` принимает custom param `otp_code`; при включённом
   2FA без него — `invalid_grant` с `error_description=2fa_required`.
   4 интеграционных теста (тест сам генерит TOTP через RFC 6238).

## Итог

**Все 5 пунктов выполнены.** Спринт 6 завершён 2026-05-28.

Сводка:
- **TD-6 RowVersion** — Postgres `xmin` через `UseXminAsConcurrencyToken`
  на 5 документах, 409 при conflict. Bonus: исправил Supply.Update.
- **TD-2 FluentValidation** — `ValidationFilter` + 5 validator'ов
  (Supply, RetailSale, Product, Counterparty, Employee).
- **TD-4 Structured logging** — `LogEnrichmentMiddleware` (OrgId/UserId/
  CorrelationId в LogContext), business-логи на Post-операциях.
- **TD-1 CQRS partial** — MediatR + 3 handler-образца с testable-абстракциями.
- **P2-4 TOTP 2FA** — endpoints + интеграция в password-grant.

**Сборка:** зелёная.
**Тесты:** 57 unit + 74 integration = **131 зелёных**.

### Что осталось вне scope автономной работы
- ОФД-оператор (`Транском`/`Касса24`),
- MoySklad webhook-токены прод,
- WPF/POS UI на Windows,
- Стейдж→прод-деплой,
- Реальный SMTP-провайдер прод (Mailgun/Sendgrid),
- Backup-коды для 2FA recovery (отдельная задача).

## Лог

- Каждый пункт: build + тесты + коммит порцией + отметка [x] + коммит прогресса.
- Все правки на `main` (origin Forgejo), без коммита `global.json`.
