Два документа после полного обхода кодовой базы: - 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>
30 KiB
ТЗ на доработку 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.