# ТЗ на доработку 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.
```
