food-market/docs/TZ-доработка.md
nns 35d70c5d80 docs: ТЗ на доработку и тестирование (полный аудит 2026-05-22)
Два документа после полного обхода кодовой базы:
- docs/TZ-доработка.md — что нужно сделать, P0/P1/P2 приоритеты,
  дорожная карта по спринтам, технический долг
- docs/TZ-тестирование.md — сценарии тестирования по модулям,
  multi-tenant изоляция, регрессионный чек-лист, стратегия
  покрытия unit/integration/E2E

Сводка готовности: ядро (auth/catalog/Supply/RetailSale/multi-tenancy)
85-95%, но критичные пробелы: ОФД, складские документы Enter/Loss/
Transfer/Inventory, Demand, отчёты, POS-приложение, observability.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 15:30:04 +05:00

222 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ТЗ на доработку Food Market
> Дата составления: 2026-05-22
> Автор анализа: Claude Opus 4.7
> Базируется на полном обходе кодовой базы `~/food-market` (backend + web + public + tests + deploy).
---
## 1. Текущее состояние системы
### 1.1. Сводная таблица готовности по модулям
| Модуль / слой | Статус | Готовность | Ключевой комментарий |
|---|---|---:|---|
| **food-market.domain** | ✅ готово | 95% | 26 сущностей, мультитенантность через `ITenantEntity`/`IOptionalTenantEntity`, чисто (нет TODO/HACK). |
| **food-market.infrastructure** | ✅ готово | 90% | EF Core 8, query filters, MailKit SMTP, StockService, MoySkladClient, 34 миграции. |
| **food-market.api (контроллеры)** | ✅ готово | 85% | 27 контроллеров, ~120 endpoint'ов, OpenIddict (password + refresh), CRUD полный. |
| **food-market.application** | 🟡 частично | 60% | Только DTO + интерфейсы, нет MediatR handlers — вся логика в контроллерах. |
| **food-market.web (админка)** | ✅ готово | 95% | 35 страниц, темная тема, адаптив, RU-локаль, onBlur-валидация. |
| **food-market.public (сайт)** | ✅ готово | 90% | Astro 4: landing, тарифы, блог, KB, legal; SignupForm → API. |
| **food-market.shared (POS контракты)** | ❌ нет | 0% | Только .csproj, ни одного CS-файла. |
| **food-market.pos.core + food-market.pos** | ❌ скелет | 5% | Пустой WPF-проект, только зависимости в .csproj. |
| **POS Sync API** | ❌ нет | 0% | Нет `/api/pos/sync`, нет `/api/pos/sales bulk`, нет WebSocket. |
| **Documents: Supply / RetailSale** | ✅ готово | 100% | Полный цикл (Draft → Post → Unpost), Stock + Movement, Cost (скользящее среднее). |
| **Documents: Inventory / Loss / Enter / Transfer** | ❌ нет | 0% | Нет контроллеров и страниц. Domain-сущности тоже не определены. |
| **Documents: Demand (оптовая отгрузка)** | ❌ нет | 0% | Только enum `MovementType.WholesaleSale`, контроллера/сущности нет. |
| **Reports** | ❌ нет | 5% | Есть `/api/sales/retail/stats` для дашборда, отдельных отчётов нет. |
| **MoySklad интеграция** | 🟡 частично | 50% | Импорт товаров и контрагентов ✅; нет Demand/Payment sync, нет webhook'ов. |
| **OpenIddict auth** | ✅ готово | 100% | Password + refresh_token; org_id, role, sub claims; persistent dev-ключи. |
| **Multi-tenancy** | ✅ готово | 95% | Query filters + `HttpContextTenantContext`; SuperAdmin override read-only/edit. |
| **Permission-based authz (RolePermissions)** | 🟡 частично | 30% | 30+ флагов в БД, но контроллеры проверяют только Roles (Admin/Cashier и т.д.). |
| **SuperAdmin Console** | ✅ готово | 95% | Organizations CRUD, audit log, archive/restore, platform settings (SMTP); биллинг-KPI заглушка. |
| **Hangfire** | 🟡 частично | 40% | `ReferencePriceRefreshJob` ✅; нет dashboard, нет scheduled cleanup, нет retry. |
| **Email (SMTP)** | ✅ готово | 100% | MailKit, DataProtection-шифрование пароля, forgot-password flow. |
| **Платёжные интеграции (Kaspi/Halyk/Jusan)** | ❌ нет | 0% | Упомянуты только в маркетинге; есть `PaymentMethod` enum, реальных шлюзов нет. |
| **ОФД (фискализация чеков РК)** | ❌ нет | 0% | Поля `FiscalSerial`/`FiscalRegNumber` есть в RetailPoint, отправки чеков нет. |
| **Маркетплейсы (Ozon, Wildberries, Kaspi Magazin)** | ❌ нет | 0% | Только маркетинговые баннеры. |
| **CI/CD (Forgejo Actions)** | ✅ готово | 90% | docker-api/web/public + smoke-тест /health после деплоя; self-hosted runner. |
| **Docker / docker-compose (stage)** | ✅ готово | 95% | postgres:16 + api + web + public + persistent volumes + local registry. |
| **E2E тесты** | 🟡 частично | 40% | Один сценарий `full-cycle` (12 шагов), отчёт в md; нет регрессии и параллелизма. |
| **Backend unit/integration тесты** | ❌ нет | 0% | Совсем. В CI стоит `\|\| echo "No tests yet"`. |
| **Logging / Serilog** | ✅ готово | 90% | Console + File с ротацией 14 дней; нет structured fields для бизнес-событий. |
| **Health checks (детальные)** | 🟡 частично | 20% | Только `/health` → {status:ok}; нет проверки БД, SMTP, диска. |
| **Метрики / observability** | ❌ нет | 0% | Нет Prometheus/AppInsights/OpenTelemetry. |
| **Rate limiting** | 🟡 частично | 15% | Только в `forgot-password` (3/час/IP, in-memory). |
| **Backup БД** | 🟡 частично | 60% | `deploy/backup.sh` есть, но не привязан к cron/timer, restore-скрипта нет. |
### 1.2. Что точно работает (готово к продакшен-использованию)
- **Регистрация → онбординг → ежедневная работа магазина** (товары, цены, приёмки, розничные продажи, остатки).
- **Управление пользователями и ролями**, soft-delete, передача владельца, восстановление пароля по email.
- **SuperAdmin-консоль платформы** (создание/архивирование организаций, SMTP, аудит).
- **Импорт каталога из МойСклад** (товары + контрагенты, асинхронный job с прогрессом).
- **Полный stage-стенд** на docker-compose с локальным registry и автодеплоем через Forgejo Actions.
### 1.3. Где точно не получится запуститься без доработки
- **Невозможно работать с физическим магазином без ККМ-фискализации** (РК требует чеки в ОФД).
- **Невозможно вести полноценный складской учёт** — нет инвентаризации, оприходования, списания, перемещения.
- **Нет аналитики/отчётов** — только сводка на дашборде, ABC-анализа, отчёта по поставщикам/прибыли нет.
- **Нет POS-приложения** — главная ценность проекта (offline-касса на Windows) — пустой проект.
- **Нет защиты от перебора паролей** в основных endpoint'ах (login/signup), только в forgot-password.
---
## 2. ТЗ на доработку по приоритетам
### Приоритет P0 — блокеры запуска в продакшен
| # | Задача | Что сделать | Зачем |
|---:|---|---|---|
| P0-1 | **Production-сертификаты OpenIddict** | Заменить `App_Data/openiddict-dev-key.xml` на реальные RSA/X.509 сертификаты, читать из KeyVault или secrets. | Сейчас токены подписываются dev-ключом без шифрования access-token. В проде это утечка claims. |
| P0-2 | **HTTPS на nginx** | Настроить TLS-termination на reverse-proxy (Let's Encrypt через certbot), форсировать HTTPS-only, добавить HSTS. | OAuth/refresh_token нельзя гонять по HTTP. |
| P0-3 | **Rate limiting на login/signup** | Добавить `Microsoft.AspNetCore.RateLimiting` (sliding window) на `/connect/token`, `/api/auth/signup`. 5 попыток/минута/IP, 20/час/IP. | Перебор паролей и DOS публичного signup. |
| P0-4 | **Health check БД** | Расширить `/health` на `/health/live` (alive) + `/health/ready` (DB ping, миграции применены). Использовать `Microsoft.Extensions.Diagnostics.HealthChecks`. | Сейчас docker-compose `healthcheck` возвращает 200 даже когда БД упала — стейдж не падает корректно. |
| P0-5 | **Permission-based authorization** | В `RolePermissions` (Domain) уже 30+ флагов. Реализовать `PermissionHandler` (IAuthorizationHandler) + атрибут `[RequiresPermission("ProductsEdit")]`, проверять в контроллерах вместо `[Authorize(Roles=...)]`. | Без этого все Admin'ы организации имеют полные права, кастомные роли (Менеджер/Кладовщик/Кассир) — фикция. |
| P0-6 | **Автоматический backup БД** | Создать systemd-timer (`food-market-backup.timer`) на ежедневный запуск `deploy/backup.sh`, добавить restore-инструкцию в `docs/`. Хранить 30 дней локально + копия в S3/MinIO. | Сейчас бэкап делается вручную, восстановления не отрепетировали. |
| P0-7 | **ОФД фискализация РК** | Интегрировать одного оператора (например, Webkassa или ОФД-Соло, КГД РК), отправлять `RetailSale.Post` чек, сохранять QR-код и фискальный номер в `RetailSale.FiscalQrCode`/`FiscalNumber`. | В РК продажа без чека ОФД — административное правонарушение. Без этого нельзя продавать. |
| P0-8 | **.env.example + документация secrets** | Описать все required env-переменные (`ConnectionStrings__DefaultConnection`, `Cors__AllowedOrigins`, `Smtp__*`, `OpenIddict__Issuer`, `OFD__Token`). Обновить `docs/stage-setup.md`. | Сейчас новый деплой не задокументирован. Передача знаний из головы — узкое место. |
| P0-9 | **Чек-листы перед релизом** | Документ `docs/release-checklist.md`: миграции применены, бэкап свежий, smoke-тесты прошли, E2E full-cycle зелёный, мониторинг здоров. | Снижает риск выкатки в проде сломанной версии. |
### Приоритет P1 — важные функциональные пробелы
| # | Задача | Что сделать | Зачем |
|---:|---|---|---|
| P1-1 | **Документ «Оприходование» (Enter)** | Domain-сущность `Enter` + `EnterLine` (как Supply, но без поставщика). Контроллер CRUD + Post/Unpost. UI-страницы `/inventory/enters`. Создаёт `StockMovement` с типом `Enter`. | Нужно вводить начальные остатки и излишки инвентаризации без поставщика. |
| P1-2 | **Документ «Списание» (Loss)** | Domain-сущность `Loss` + `LossLine` (причина: брак, истечение срока, бой, недостача). Контроллер + UI. `StockMovement` тип `WriteOff`. | Списание брака — обязательная функция магазина. |
| P1-3 | **Документ «Перемещение» (Transfer)** | Domain `Transfer` + `TransferLine` (FromStore → ToStore). Контроллер с атомарной транзакцией (списание + поступление). UI-форма. | В сети магазинов товар постоянно перемещается между складами. |
| P1-4 | **Документ «Инвентаризация» (Inventory)** | Domain `Inventory` + `InventoryLine` (book qty, actual qty, diff). Контроллер с импортом текущих остатков + Post создаёт корректирующее движение `InventoryAdjustment`. UI-форма с CSV-импортом фактического количества. | Регулярная сверка остатков — обязательно для розницы. |
| P1-5 | **Документ «Оптовая отгрузка» (Demand)** | Domain `Demand` + `DemandLine` (покупатель, способ оплаты — наличные/безнал, цена опт.). Контроллер. UI-страницы. `StockMovement` тип `WholesaleSale`. | Часть клиентов работает с юрлицами, отгрузка по накладной с НДС. |
| P1-6 | **Возврат от покупателя (CustomerReturn)** | Расширить `RetailSale` опцией «Возврат» (по чеку или без). Domain enum `MovementType.CustomerReturn` уже есть. UI: кнопка «Создать возврат» из посту-проведённой продажи. | Закон о защите прав потребителей в РК требует приёма возвратов. |
| P1-7 | **Возврат поставщику (SupplierReturn)** | По аналогии с CustomerReturn для Supply. UI: «Возврат поставщику» из проведённой приёмки. | Брак, неликвид, отказ от партии. |
| P1-8 | **Отчёт «Продажи»** | `/api/reports/sales` с группировкой по периодам (день/неделя/месяц), товарам, кассирам, кассам, способам оплаты. UI: страница `/reports/sales` с фильтром периода и экспортом в CSV/XLSX. | Без отчёта по продажам управлять бизнесом невозможно. |
| P1-9 | **Отчёт «Остатки на дату»** | `/api/reports/stock` с восстановлением остатков на любую дату через `StockMovement` журнал. UI с экспортом. | Налоговый учёт, инвентаризация. |
| P1-10 | **Отчёт «Прибыль»** | `/api/reports/profit` — выручка - себестоимость по периодам/группам/товарам. Используем `Cost` snapshot из `RetailSaleLine`. | Главный показатель магазина. |
| P1-11 | **Отчёт «ABC-анализ»** | Топ товаров по выручке/прибыли/маржинальности за период. Группа A/B/C по правилу Парето. | Управление ассортиментом. |
| P1-12 | **POS Sync API** | Endpoints: `GET /api/pos/sync?since={ts}` (товары, цены, остатки, контрагенты с изменениями после ts); `POST /api/pos/sales` (батч продаж с idempotency-key). Контракты в `food-market.shared`. | Без этого POS-приложение не может синхронизироваться с сервером. |
| P1-13 | **POS WPF MVP** | Минимальный UI: логин кассира (привязка к RetailPoint), список товаров/поиск по штрихкоду, корзина, оплата (нал/карта), печать чека (с ОФД), оффлайн-буфер на SQLite, фоновая синхронизация. | Главная фича проекта по позиционированию. |
| P1-14 | **MoySklad — Demand sync** | Импорт оптовых отгрузок (демандов) из МойСклад. Расширить `MoySkladImportService`. | Текущая интеграция только односторонняя для каталога; продажи не синхронизируются. |
| P1-15 | **MoySklad — webhook на изменения** | Получать webhook'и от МойСклад при изменении товаров, автоматически обновлять каталог (вместо ручного «Импортировать сейчас»). | Двусторонняя живая синхронизация. |
| P1-16 | **Hangfire dashboard** | Подключить `Hangfire.Dashboard` с авторизацией только для SuperAdmin. Добавить scheduled jobs: ежедневный cleanup `StockMovement` (старше 2 лет), audit-log (старше 90 дней), eтиничные jobs (e.g. рассылка email). | Сейчас jobs запускаются только вручную через AdminCleanupController; нет видимости. |
| P1-17 | **Метрики Prometheus** | Подключить `prometheus-net.AspNetCore` (`/metrics` endpoint). Базовый набор: http_requests_total, http_request_duration, db_query_duration, business: sales_count, supply_posted_count, errors_total. | Без observability нельзя гнать прод. |
| P1-18 | **Аудит мутаций tenant'а** | Расширить `SuperAdminAuditLog` на обычные org-мутации (`OrgAuditLog`): кто, когда, что изменил в Supply/Sale/Product/Counterparty. Хранить diff JSON. | Розница часто судится с сотрудниками по поводу пропавших товаров — нужны доказательства. |
| P1-19 | **OpenAPI спецификация** | Включить `Swashbuckle.AspNetCore`. Опубликовать `/swagger/v1/swagger.json` (только в Dev) и сгенерировать TypeScript-клиент для food-market.web. | Удалит ручной труд по типизации API в фронте и POS. |
| P1-20 | **Unit-тесты критичной логики** | Покрыть xUnit'ом: `StockService.ApplyMovement`, расчёт Cost при `SuppliesController.Post`, расчёт автонаценки по `ProductGroup.MarkupPercent`, валидация платежа `RetailSalesController.Post`, multi-tenant query filter. | Без этих тестов любое изменение логики Supply/Sale = потенциально баг с минусовыми остатками или потерями денег. |
| P1-21 | **Integration-тесты на тестовой БД** | `Testcontainers.PostgreSql` + `WebApplicationFactory`. Покрыть: signup-flow, supply post→unpost, retail sale post с overselling, tenant isolation (org A vs org B), permission проверки. | Регрессия на каждый коммит в CI. |
| P1-22 | **Email-нотификации** | Готовый MailKit-сервис расширить шаблонами: приглашение сотрудника (с временным паролем), еженедельный отчёт владельцу, low-stock alert. Хранить шаблоны в `Resources/EmailTemplates/*.html`. | Сейчас email отправляется только при forgot-password. |
### Приоритет P2 — желательные улучшения
| # | Задача | Что сделать | Зачем |
|---:|---|---|---|
| P2-1 | **Платёжный шлюз Kaspi Pay** | Интеграция Kaspi Pay QR (касса показывает QR, покупатель оплачивает с приложения, callback фиксирует оплату в RetailSale). | Самый популярный способ безнала в РК. |
| P2-2 | **Платёжные шлюзы банков** | Halyk Epay, Jusan Pay, Forte Pay (POS-терминал API или e-commerce). | Альтернативы Kaspi. |
| P2-3 | **Интеграция с маркетплейсами** | Ozon Seller API, Wildberries, Kaspi Magazin — синхронизация остатков и цен (исходящая), импорт заказов (входящая). | Расширение каналов продаж. |
| P2-4 | **2FA для админов** | TOTP (Google Authenticator) для роли Admin и SuperAdmin. Использовать `Identity.AddDefaultTokenProviders` + `AuthenticatorTokenProvider`. | Защита платёжного функционала. |
| P2-5 | **SSO (Google/Microsoft)** | Расширить OpenIddict внешними провайдерами для логина персонала. | UX для офисных сотрудников. |
| P2-6 | **Многоязычность (en/kz)** | Подключить `react-i18next` в web, выделить русские строки в `locales/ru.json`. Перевести интерфейс на казахский (государственное требование). | Государство РК требует госязык в публичных интерфейсах. |
| P2-7 | **WebSocket / SignalR для real-time** | Push-уведомления на дашборд (новая продажа), кассе (изменение цены), импортах (вместо polling). | UX + снижение нагрузки от polling. |
| P2-8 | **Аналитика на public-сайте** | Google Analytics или Yandex.Metrika, A/B тесты pricing'а, события signup-конверсии. | Маркетинг. |
| P2-9 | **Mobile-приложение (PWA или React Native)** | Просмотр остатков, продаж, KPI для владельца. | UX для владельцев. |
| P2-10 | **Распознавание чеков (OCR)** | Загрузка фото чека от поставщика → распознавание → автозаполнение Supply. | Уменьшение ручного ввода. |
| P2-11 | **Электронные счёт-фактуры (ЭСФ)** | Интеграция с ИС ЭСФ КГД РК (выпуск счетов-фактур для юрлиц). | Часть оптовых клиентов требует ЭСФ. |
| P2-12 | **Бонусные программы / скидочные карты** | Domain: `LoyaltyProgram`, `LoyaltyCard`. Списание/начисление в RetailSale. | Удержание клиентов. |
| P2-13 | **Промокоды / акции** | Domain: `Promotion`, правила (категория, период, % скидки). UI-настройка из админки. | Маркетинг для магазина. |
| P2-14 | **Telegram-бот для владельца** | Ежедневная сводка выручки, low-stock alerts. | UX для владельцев. |
| P2-15 | **Multi-storage для изображений** | Сейчас файлы лежат в `/app/uploads` (volume). Перевести на S3-совместимое хранилище (MinIO/Yandex.Cloud). | Масштабируемость, отказоустойчивость. |
---
## 3. Дорожная карта (рекомендованная последовательность)
### Спринт 1 — Стабилизация (2-3 недели)
Цель: безопасно выкатить текущий функционал в прод.
- P0-1 → P0-9 (все блокеры запуска)
- P1-20, P1-21 (юнит/интеграционные тесты на текущую логику)
- P1-18 (аудит мутаций tenant'а)
**Критерий готовности:** прод-стенд работает с HTTPS, rate-limit'ы установлены, бэкап автоматический, фискализация ОФД работает, права RolePermissions проверяются.
### Спринт 2 — Складской учёт (3-4 недели)
Цель: полноценное складское ядро ERP.
- P1-1 (Enter), P1-2 (Loss), P1-3 (Transfer), P1-4 (Inventory)
- P1-6 (CustomerReturn), P1-7 (SupplierReturn)
- P1-16 (Hangfire dashboard + scheduled cleanup)
**Критерий готовности:** магазин может вести полный складской учёт без обходных путей.
### Спринт 3 — Отчёты и аналитика (2 недели)
- P1-8 (Sales report), P1-9 (Stock on date), P1-10 (Profit), P1-11 (ABC)
- P1-19 (OpenAPI / Swagger)
**Критерий готовности:** владелец видит, как идёт бизнес, без выгрузки в Excel.
### Спринт 4 — POS (4-6 недель)
- P1-12 (POS Sync API), `food-market.shared` контракты
- P1-13 (POS WPF MVP)
- P1-17 (метрики Prometheus + Grafana dashboard)
**Критерий готовности:** касса работает оффлайн, синхронизируется с сервером, печатает фискальные чеки.
### Спринт 5 — Оптовые продажи + MoySklad full sync (2-3 недели)
- P1-5 (Demand)
- P1-14 (MoySklad Demand sync), P1-15 (webhook'и)
- P1-22 (email-шаблоны)
**Критерий готовности:** клиент, работающий с юрлицами через МойСклад, может полностью перейти на Food Market.
### Спринт 6+ — Интеграции и фичи (P2)
P2-1 Kaspi Pay → P2-3 маркетплейсы → P2-6 локализация → P2-11 ЭСФ → P2-12/13 лояльность/акции.
---
## 4. Технический долг (для рефакторинга)
Не блокирует функциональность, но затрудняет развитие.
| # | Что | Почему важно |
|---:|---|---|
| TD-1 | **CQRS через MediatR** — перенести бизнес-логику из контроллеров в Command/Query handlers. | Сейчас невозможно переиспользовать логику между API/POS/Hangfire. Контроллеры по 500 строк. |
| TD-2 | **FluentValidation** — заменить inline-валидацию в контроллерах на отдельные `Validator<T>`. | Сейчас валидация перемешана с бизнес-логикой, тестировать сложно. |
| TD-3 | **Mapster** — выделить mapping в отдельные `MapperConfig`. | Сейчас projection'ы инлайнятся в LINQ-запросы, переиспользования нет. |
| TD-4 | **Структурные log-fields в Serilog** — добавить `org_id`, `user_id`, `correlation_id` в log scope. | Сейчас в логах сложно найти конкретного пользователя/организацию. |
| TD-5 | **ImportJobRegistry в БД** — сейчас in-memory `ConcurrentDictionary`. При рестарте API теряется. Перевести на таблицу `ImportJobs`. | Жизненный цикл job'а >5 минут — рестарт обычное дело. |
| TD-6 | **Concurrency-токены на документах**`RowVersion` (xmin/timestamp) на Supply/RetailSale, чтобы исключить race condition при параллельной правке. | Сейчас два кассира могут испортить один чек. |
---
## 5. Сводка по оценке готовности
```
┌──────────────────────────────────┬──────────────┬─────────────┐
│ Категория │ Готовность │ Состояние │
├──────────────────────────────────┼──────────────┼─────────────┤
│ Авторизация и multi-tenancy │ 95% │ ✅ готово │
│ Каталог товаров │ 95% │ ✅ готово │
│ Документы (Supply, RetailSale) │ 100% │ ✅ готово │
│ Документы (Inventory/Loss/...) │ 0% │ ❌ нет │
│ Отчёты │ 5% │ ❌ нет │
│ POS │ 5% │ ❌ нет │
│ MoySklad │ 50% │ 🟡 частично │
│ Платежи и фискализация │ 0% │ ❌ нет │
│ Инфраструктура (CI/CD, Docker) │ 90% │ ✅ готово │
│ Безопасность (HTTPS, rate-limit) │ 30% │ 🟡 частично │
│ Observability (метрики, аудит) │ 20% │ 🟡 частично │
│ Тестирование │ 40% │ 🟡 частично │
└──────────────────────────────────┴──────────────┴─────────────┘
Общая готовность к продакшен-запуску: 60-65%
- Для MVP "магазин на одном POS-терминале": требуется ОФД + базовые складские документы.
- Для полноценного ERP: требуется выполнение P0+P1.
- Для конкуренции с МойСклад: требуется ещё и P2.
```