# food-market — архитектура

Документ для разработчика, который пришёл в проект первый раз. Описывает
слои, модули, ключевые потоки и почему некоторые вещи сделаны именно так.

Старая короткая версия — `docs/architecture.md` (lowercase). Этот файл
заменяет её и расширяет.

## TL;DR

- **Что**: multi-tenant SaaS-аналог МойСклад для розничных магазинов РК.
- **Backend**: .NET 8 LTS, ASP.NET Core, EF Core 8, PostgreSQL 14+ (dev) / 16 (prod).
- **Auth**: OpenIddict 5 (password + refresh) поверх ASP.NET Identity.
- **Web**: React 19 + Vite + TS, Tailwind v4, shadcn/ui, TanStack Query, AG Grid.
- **POS**: WPF на .NET 8 Windows, оффлайн-буфер в SQLite, синк через `/api/pos/v1`.

## Топология deployment

```
┌─────────────────────────────────────────────────────────────────┐
│                  Internet / LAN магазина                         │
└───────────┬───────────────────────┬─────────────────────────────┘
            │ HTTPS                  │ HTTPS (Bearer) + офлайн-буфер
            ▼                        ▼
   ┌────────────────────┐    ┌──────────────────────────┐
   │  food-market.web   │    │  food-market.pos (WPF)    │
   │   React SPA        │    │   .NET 8, Windows 10+     │
   │   admin.fm.kz      │    │   локальная SQLite        │
   └─────────┬──────────┘    └──────────┬───────────────┘
             │                          │
             │ /api/*                   │ /api/pos/v1/*
             │ /hubs/notifications      │
             └─────────────┬────────────┘
                           ▼
          ┌───────────────────────────────────────────┐
          │         food-market.api                    │
          │   ASP.NET Core + OpenIddict + SignalR      │
          │  - tenant query filters per request        │
          │  - Hangfire scheduler + recurring jobs     │
          │  - /metrics (Prometheus) /health/{live,ready}│
          └────┬──────────┬───────────┬───────────┬───┘
               ▼          ▼           ▼           ▼
         ┌────────┐ ┌─────────┐ ┌──────────┐ ┌──────────┐
         │Postgres│ │ Hangfire│ │ MinIO    │ │ Logs     │
         │  16    │ │  (jobs) │ │ (S3, opt)│ │ Serilog  │
         └────────┘ └─────────┘ └──────────┘ └──────────┘
                                   │
                                   ▼
                        локальный FS (/uploads volume)
                          — если MinIO не настроен
```

Stage и prod крутятся через `deploy/docker-compose.yml` на dev-vm
(`192.168.1.190`). Локальный dev: API на `:5081`, Postgres из
brew (`postgres@14` на `:5432`), web через `pnpm dev` на `:5173`.

## Структура солюшна

```
food-market/
├── src/
│   ├── food-market.domain/          ← POCO, enum, доменные интерфейсы
│   ├── food-market.application/     ← MediatR-handlers, DTO, абстракции
│   ├── food-market.infrastructure/  ← EF Core, Identity, OpenIddict EF, внешние API
│   ├── food-market.api/             ← ASP.NET Core host: controllers, middleware, DI
│   ├── food-market.web/             ← React SPA
│   ├── food-market.shared/          ← DTO-контракты api ↔ pos
│   ├── food-market.public/          ← Astro static (маркетинг food-market.kz)
│   ├── food-market.pos.core/        ← логика POS (без UI)
│   └── food-market.pos/             ← WPF UI (net8.0-windows)
├── tests/
│   ├── food-market.UnitTests/       ← xUnit + InMemoryDB
│   ├── food-market.IntegrationTests/← xUnit + Testcontainers Postgres
│   ├── e2e/                         ← Playwright (TS), бьёт по test.admin.food-market.kz
│   └── load/                        ← k6 (Sprint 12)
├── deploy/                          ← docker-compose, Dockerfile.*, systemd-юниты
└── docs/                            ← вы здесь
```

### Слои (Clean Architecture)

| Слой              | Зависит от                | Что лежит                                                                              |
|-------------------|---------------------------|----------------------------------------------------------------------------------------|
| **domain**        | ничего                    | POCO-сущности, enum'ы, доменные интерфейсы (`ITenantEntity`, `IVersionedEntity`).      |
| **application**   | domain + shared           | MediatR `IRequest`/`IRequestHandler`, DTO, абстракции (`IFiscalProvider`, `IEmailSender`, `IStockService`, `ITenantContext`), `FluentValidation` валидаторы. |
| **infrastructure**| application + domain      | `AppDbContext`, Identity-таблицы, OpenIddict EF store, реализации абстракций, HTTP-клиенты к внешним API (Webkassa, MoySklad, MailKit, Telegram). |
| **api**           | всё перечисленное выше    | ASP.NET Core host: контроллеры, middleware, DI-проводка, фоновые джобы (Hangfire), Realtime hub'ы (SignalR), сидеры. |

Правило одностороннего направления зависимостей: домен не знает про EF и
ASP.NET, application — про конкретные провайдеры. Это позволило прикрутить
ОФД (Sprint 11) одним интерфейсом + четырьмя реализациями, без правок
контроллеров кроме одной точки вызова.

## Модули backend

### Domain (`src/food-market.domain/`)

- `Common/Entity.cs` — базовая `Entity` с `Id/CreatedAt/UpdatedAt`.
- `Common/TenantEntity.cs` — `ITenantEntity` (обязательный `OrganizationId`),
  `TenantEntity` (база), `IOptionalTenantEntity` (системные справочники с
  `OrganizationId?`).
- `Common/IVersionedEntity.cs` — оптимистичная блокировка через PG `xmin`
  (`Xmin` поле).
- Бизнес-сущности по поддоменам: `Catalog/` (Product, Counterparty,
  ProductGroup, …), `Inventory/` (Stock, StockMovement, Loss, Transfer,
  Inventory), `Purchases/` (Supply, Enter, SupplierReturn),
  `Sales/` (RetailSale, RetailSaleLine, Demand, LoyaltyCard,
  LoyaltyProgram, Promotion), `Organizations/` (Organization, Employee,
  EmployeeRole, OrgAuditLog, SuperAdminAuditLog), `Platform/`
  (PlatformSettings — singleton SMTP-конфиг).

### Application (`src/food-market.application/`)

- **CQRS на MediatR** — пока partial: образцы в `Purchases/Commands/CreateSupplyCommand.cs`,
  `Sales/Commands/PostRetailSaleCommand.cs`, `Sales/Queries/GetSalesReportQuery.cs`.
  Большинство контроллеров пока «толстые» (исторически до TD-1).
- **Абстракции**:
  - `Common/Tenancy/ITenantContext` — `OrganizationId`, `IsSuperAdmin`,
    `IsTenantOverride`, `UserId`.
  - `Common/Email/IEmailSender` — отправка через текущий SMTP-конфиг.
  - `Common/Fiscal/IFiscalProvider` + `IFiscalProviderFactory` (Sprint 11).
  - `Inventory/IStockService` — единая точка списания/начисления остатка
    (любая операция, меняющая склад, идёт через `ApplyMovementAsync`).
- **FluentValidation** валидаторы рядом с DTO; глобально подключаются
  через `AddValidatorsFromAssemblyContaining<Program>()`.

### Infrastructure (`src/food-market.infrastructure/`)

- `Persistence/AppDbContext.cs` — единый DbContext (тенанта + Identity +
  OpenIddict EF store). Query-filter применяется через reflection ко всем
  `ITenantEntity` (см. [MULTI-TENANCY.md](MULTI-TENANCY.md)).
- `Persistence/Configurations/*.cs` — EF Core fluent configs по поддоменам.
- `Persistence/Migrations/` — миграции пишутся вручную (см. CLAUDE.md /
  memory `feedback_ef_migrations`), снапшот не синхронизируется
  с моделью (используется только `dotnet ef migrations add`, который
  не вызывается в этом проекте).
- `Persistence/OrgAuditInterceptor.cs` — EF `ISaveChangesInterceptor`,
  пишет каждую `Add/Update/Delete` в `org_audit_log` (JSONB diff).
- `Identity/` — кастомные `User`, `Role` для ASP.NET Identity.
- `Email/MailKitEmailSender.cs` — SMTP через MailKit, конфиг из
  `PlatformSettings` (читается на каждой отправке через scope).
- `Fiscal/` — `IFiscalProvider` реализации: Mock + Webkassa (полный) +
  Kassa24/OfdSolo (skeleton). См. [ofd-integration.md](ofd-integration.md).
- `Inventory/StockService.cs` — единственное место, где двигаются остатки.
  Бизнес-инвариант: stock = SUM(stock_movements) per (productId, storeId).
- `Integrations/MoySklad/` — HTTP-клиент + конвертер для импорта каталога.

### Api (`src/food-market.api/`)

- `Program.cs` — composition root (~570 строк, поделён логическими
  блоками; см. секцию «Composition root» ниже).
- `Controllers/` — REST-API. Структура совпадает с маршрутами:
  - `Auth/` — `/api/auth/*` (signup, forgot-password, 2FA).
  - `Catalog/` — `/api/catalog/{products,counterparties,…}`.
  - `Purchases/` — `/api/purchases/{supplies,supplier-returns}`.
  - `Sales/` — `/api/sales/{retail,demands}`.
  - `Inventory/` — `/api/inventory/{stock,enters,losses,transfers,inventories}`.
  - `Reports/` — `/api/reports/{sales,stock,profit,abc}`.
  - `Dashboard/` — `/api/dashboard/{top-products,low-stock,recent-sales,margin}`.
  - `Loyalty/`, `Promotions/` — Sprint 9.
  - `Organizations/` — настройки орги, сотрудники, роли, ОФД.
  - `Pos/` — `/api/pos/v1/*` для WPF POS (sync, idempotency).
  - `SuperAdmin/` — `/api/super-admin/*` (управление платформой).
  - `Admin/` — `/api/admin/*` (per-org admin tools: cleanup, demo-seed,
    moysklad-import, audit-log просмотр).
  - `Search/` — глобальный `/api/search/global` (Cmd+K).
  - `Telegram/` — bind owner-chat, статус.
  - `Uploads/` — multipart upload изображений.
- `Infrastructure/`:
  - `Tenancy/HttpContextTenantContext.cs` — реализация `ITenantContext`
    через `IHttpContextAccessor` + AsyncLocal-override для background tasks.
  - `Tenancy/SuperAdminOverrideClaimsTransformer.cs` — добавляет
    `Admin/Cashier/Storekeeper` роли SuperAdmin'у с активным
    `X-Org-Override`, чтобы `[Authorize(Roles="Admin")]` не отшил его.
  - `Tenancy/ReadonlyOverrideMiddleware.cs` — в режиме override без
    `X-Org-Override-Reason` блочит любую мутацию (читать всё, писать
    ничего; писать — только в edit-mode с reason).
  - `Tenancy/SuperAdminEditAuditFilter.cs` — глобальный action-filter,
    при mutate-в-override пишет в `super_admin_audit_log`.
  - `Authorization/RequiresPermissionAttribute.cs` + `PermissionAuthorizationPolicyProvider`
    + `PermissionAuthorizationHandler` — permission-based авторизация.
    `[RequiresPermission("ProductsEdit")]` → policy `perm:ProductsEdit` →
    `RolePermissions.ProductsEdit` булева на `EmployeeRole`.
  - `Validation/ValidationFilter.cs` — FluentValidation → 400
    ProblemDetails (RFC 7807).
  - `RateLimiting/AuthRateLimiterExtensions.cs` — 5/мин + 20/час на
    `/connect/token`, `/api/auth/signup` по IP+username.
  - `Observability/LogEnrichmentMiddleware.cs` — кладёт
    `CorrelationId/OrgId/UserId` в Serilog `LogContext`, каждая запись
    в журнале получает эти лейблы.
  - `Observability/DbMetricsInterceptor.cs` — EF интерсептор, Prometheus
    `food_market_db_query_duration_seconds`.
  - `Observability/AppMetrics.cs` — статические Counter'ы (Posted/Unposted
    per docType, FiscalRegistered, …).
  - `Health/DatabaseReadyHealthCheck.cs` — `SELECT 1` + проверка
    `__EFMigrationsHistory`.
  - `Security/OpenIddictKeyConfigurator.cs` — в dev — persistent RSA в
    `App_Data/oidc-keys/*`; в stage/prod — X509 PFX из конфига
    (см. [openiddict-keys.md](openiddict-keys.md)).
- `Realtime/NotificationsHub.cs` + `NotificationsPublisher.cs` —
  SignalR-хаб `/hubs/notifications`, группы per-org. События:
  `SalePosted`, `LowStock`, `ImportProgress`.
- `Background/`:
  - `HangfireJobsConfigurator` — регистрирует recurring jobs при старте:
    `prune-stock-movements` (03:30), `prune-audit-log` (03:45),
    `weekly-summary` (пн 07:00), `low-stock-alert` (08:00),
    `telegram-owner-daily-summary` (06:00).
  - `HousekeepingJobs` — pg-cleanup'ы.
  - `EmailNotificationJobs` — weekly-summary + low-stock email.
  - `OwnerDailySummaryJob` — Telegram-сводка владельцу.
  - `ReferencePriceRefreshJob` — пересчёт `Product.ReferencePrice`
    каждые 30 дней без приёмок.
- `Seed/`:
  - `SystemReferenceSeeder` — справочники (страны, валюты, единицы).
  - `OpenIddictClientSeeder` — регистрирует client `food-market-web`.
  - `DevDataSeeder` — dev-only admin user (SuperAdmin).
  - `DemoTenantSeeder` / `YearDemoSeeder` — заполняют tenant
    демо-данными (Sprint 5 / Sprint 10).

### Web (`src/food-market.web/`)

- Vite + React 19 + TS 6, Tailwind v4. Маршрутизация — React Router 6.
- `src/lib/api.ts` — axios instance с auto-refresh токена.
- `src/lib/auth.ts` — login/logout, store токена в `localStorage`.
- `src/components/` — общие виджеты (Field, Button, Skeleton,
  CommandPalette, DashboardWidgets).
- `src/pages/` — страницы (один файл per route).
- TanStack Query — кеширование API-вызовов, инвалидация по SignalR.
- AG Grid Community — большие списки (товары, контрагенты, отчёты).

### POS (`src/food-market.pos*/`)

- `pos.core/` — логика без UI: оффлайн-буфер, sync, расчёт чека.
- `pos/` — WPF UI, CommunityToolkit.Mvvm, SQLite, Refit + Polly,
  System.IO.Ports для весов CAS.
- Sync: батчем по 50 чеков через `POST /api/pos/v1/batch` с
  `Idempotency-Key`. Сервер дедупит через `pos_batch_acks` уникальный
  индекс (`OrganizationId, IdempotencyKey`).

## Composition root (`Program.cs`)

Логические блоки в порядке регистрации:

1. **Serilog** bootstrap (до builder).
2. **CORS** (`Cors:AllowedOrigins` из конфига).
3. **HttpContextAccessor** + `ITenantContext` + `IClaimsTransformation`
   (SuperAdmin override роли).
4. **EF Core**: `AppDbContext` (Npgsql, OpenIddict, два interceptor'а).
5. **Identity** + **OpenIddict** server (password + refresh, rolling
   refresh, leeway = 0).
6. **Authentication/Authorization** policies (`AdminAccess`, `perm:*`).
7. **Rate-limiter** (`/connect/token`, `/api/auth/signup`).
8. **HealthChecks** (`database` тег `ready`).
9. **IEmailSender** (Singleton + scope для DbContext).
10. **IFiscalProvider** + 3 HttpClient + `IFiscalProviderFactory`.
11. **MediatR** (assembly scan), **FluentValidation**.
12. **MoySklad HttpClient** + import service.
13. **Hangfire** server + storage (PG), `HangfireJobsConfigurator` хостед.
14. **SignalR** + `INotificationsPublisher`.
15. **Telegram-бот** HttpClient (если token задан).
16. **Сидеры**: `OpenIddictClientSeeder`, `SystemReferenceSeeder`,
    `DevDataSeeder` хостед; `DemoTenantSeeder`/`YearDemoSeeder` scoped.
17. `Build()` → middleware pipeline (Serilog→CORS→HttpMetrics→
    RateLimiter→hubs-token-fix→AuthN→AuthZ→LogEnrichment→
    ReadonlyOverride→StaticFiles[/uploads]→Swagger→MapControllers→
    MapHub→MapMetrics→HangfireDashboard→HealthChecks).
18. На старте: `db.Database.Migrate()` (идемпотентно).
19. `app.Run()`.

## Поток: signup → bootstrap → первая продажа

```
1. POST /api/auth/signup { email, password, organizationName, phone }
   ─→ создание Organization (Entity, не tenant-scoped)
   ─→ создание AppUser + добавление в роль "Admin"
   ─→ создание Employee с AdminRole и всеми permission'ами
   ─→ создание главного Store (isMain=true) + RetailPoint
   ─→ создание PriceType "Розничная" (isRetail=true, isSystem=true)
   ─→ копирование системных UnitOfMeasure (OrganizationId=null) на org

2. POST /connect/token { grant_type=password, username, password }
   ─→ OpenIddict проверяет, выдаёт access_token + refresh_token
   ─→ access_token содержит claim org_id и role

3. GET /api/me  (web bootstrap)
   ─→ возвращает { sub, email, roles, orgId, hasLiveOrg, hasActiveEmployee }
   ─→ фронт роутит на /dashboard или /no-organization (orphan-fallback)

4. POST /api/catalog/products  { name, prices, barcodes, ... }
   ─→ ValidationFilter (FluentValidation)
   ─→ controller → _db.Products.Add(...) (OrganizationId stamped в SaveChanges)
   ─→ возвращает Product DTO

5. POST /api/purchases/supplies + POST /{id}/post
   ─→ post идёт под Serializable tx
   ─→ для каждой строки StockService.ApplyMovementAsync(+qty, MovementType.Supply)
   ─→ Stock row для (productId, storeId) либо создаётся, либо обновляется
   ─→ commit, AppMetrics.IncrementPosted("supply")
   ─→ SignalR: NotificationsHub → группа org → событие SupplyPosted

6. POST /api/sales/retail + POST /{id}/post
   ─→ Serializable tx, проверка остатка ≥ 0 для каждой строки
   ─→ StockService.ApplyMovementAsync(-qty, MovementType.RetailSale)
   ─→ commit, AppMetrics.IncrementPosted("retail-sale")
   ─→ best-effort TryFiscalizeAsync (Sprint 11) — отдельно, после commit
   ─→ SignalR: SalePosted (dashboard виджеты инвалидируют queries)
```

## База данных

Postgres 14+ для dev (brew systemwide), Postgres 16 в Docker для
stage/prod. Названия таблиц snake_case через явный `ToTable("…")`.

### Ключевые таблицы

| Таблица | Назначение |
|---|---|
| `organizations` | Корневой tenant. Не tenant-scoped. |
| `users`, `roles`, `user_roles` | ASP.NET Identity. |
| `employees`, `employee_roles`, `role_permissions` | Сотрудники tenant'а + кастомные роли с булевыми флагами прав. |
| `products`, `product_prices`, `product_barcodes`, `product_images`, `product_groups` | Каталог товаров. |
| `counterparties` | Поставщики + покупатели (тип=Supplier/Individual/Legal). |
| `stores`, `retail_points`, `units_of_measure`, `currencies`, `price_types`, `countries` | Справочники. |
| `stocks`, `stock_movements` | Остатки + история движений. `stocks` — кеш `SUM(stock_movements)`. |
| `supplies`, `supply_lines`, `enters`, `enter_lines`, `supplier_returns`, `supplier_return_lines` | Приходные документы. |
| `losses`, `loss_lines`, `transfers`, `transfer_lines`, `inventory_docs`, `inventory_lines` | Внутренний учёт. |
| `retail_sales`, `retail_sale_lines` | Чеки розницы + строки чека. Sprint 11: ОФД-снапшоты на `retail_sales` (FiscalNumber, FiscalQrCode, …). |
| `demands`, `demand_lines` | Опт-отгрузки. |
| `loyalty_programs`, `loyalty_cards`, `promotions` | Sprint 9. |
| `pos_batch_acks` | Идемпотентность POS-синка (UNIQUE OrganizationId, IdempotencyKey). |
| `org_audit_log` | JSONB-diff каждой mutate-операции tenant'а. |
| `super_admin_audit_log` | Действия SuperAdmin'а (особенно в режиме «открыто как…»). |
| `platform_settings` | Singleton: SMTP-конфиг платформы. |
| `system_settings` | Singleton: per-tenant фичи (не путать с platform). |
| `import_jobs` | История импортов MoySklad. |

OpenIddict хранит `openiddict_applications`/`authorizations`/`tokens`/`scopes`.
Hangfire — `hangfire.*` (в своей схеме).

### Concurrency

`IVersionedEntity` сущности (Supply, RetailSale, Demand, Enter, Loss,
Transfer, InventoryDoc, SupplierReturn) включают PG `xmin` через
`UseXminAsConcurrencyToken()`. Параллельные апдейты одного документа
получают `DbUpdateConcurrencyException`, контроллер возвращает 409.

Post-операции, изменяющие остаток, идут под `IsolationLevel.Serializable`
(см. `RetailSalesController.Post`, `SuppliesController.Post`, …) —
это защищает от race в `SUM(stock_movements)`-инварианте.

## Внешние интеграции

| Сервис | Где | Состояние |
|---|---|---|
| **MoySklad** | `Infrastructure/Integrations/MoySklad/` | Импорт товаров, контрагентов, остатков. Per-org token в `Organization.MoySkladToken`. |
| **SMTP** | `Infrastructure/Email/MailKitEmailSender.cs` | Платформенный SMTP в `PlatformSettings` (SuperAdmin настраивает). Используется для invite, forgot-password, weekly-summary. |
| **Telegram Bot** | `Api/Integrations/Telegram/` | Owner-сводка. Per-org `OwnerTelegramChatId`. Bot token в env. |
| **ОФД (Webkassa / Kassa24 / ОФД-Соло)** | `Infrastructure/Fiscal/` | Sprint 11 scaffolding. Per-org провайдер + креды. |
| **MinIO (S3)** | `Api/Storage/StorageBootstrap.cs` | Опциональный сторадж изображений. Если не настроен — `/uploads` volume на FS. |

## Тесты

- **Unit** (`tests/food-market.UnitTests/`) — xUnit + InMemory EF + чистые
  юниты на валидаторы, payload-builder'ы, MediatR-handler'ы.
- **Integration** (`tests/food-market.IntegrationTests/`) — xUnit +
  Testcontainers Postgres (`postgres:16-alpine`). Полный API через
  `WebApplicationFactory<Program>`. Shared `ApiFactory` через
  `ApiCollection` (один контейнер на сессию xunit). Memory note:
  `test_suites_setup` — Ryuk выключен (TCN не тянет с docker-hub),
  rate-limiter eager-config через env-переменную.
- **E2E** (`tests/e2e/`) — Playwright (TS) против stage
  `https://test.admin.food-market.kz`. Используется в verify-suite'ах
  по спринтам.
- **Load** (`tests/load/`) — k6 (Sprint 12). См. `docs/performance-baseline.md`.

### Sprint 13-22 changes (быстрая сводка)

| Sprint | Что добавлено / изменено |
|---|---|
| **13** (security) | `SecurityHeadersMiddleware` (CSP, X-Frame, HSTS); rate-limit на signup (3/h IP) и forgot-password (3/h email + 10/h IP); `SensitiveOpsAudit` сервис для logged-ops; `POST /api/me/sessions/revoke-all` через `IOpenIddictAuthorizationManager`; Hangfire-dashboard под `SuperAdminHangfireFilter` + nginx /hangfire location. Также: dedicated PG-роль `food_market_server_app` для legacy back.food-market.kz (без superuser). |
| **14** (perf) | Phase14a индексы (composite + partial с INCLUDE на `retail_sales`); N+1 fix в `SalesReportController.FetchAsync`; React.lazy на 30 редких страниц + Recharts lazy → bundle 1456→706 KB (−51%); ImageSharp генерирует thumb/medium WebP при загрузке + `<ProductImage>` с `<picture>` srcset; Npgsql pool (Min=10/Max=100/AutoPrepare=20); `JobTimingFilter` для Hangfire-jobs. |
| **15** (a11y + tests) | `useFocusTrap` (WCAG 2.4.3/2.1.2) на Modal + ConfirmDialog; axe-core spec-suite (10 страниц, 0 critical); aria-label на icon-only back-links + role="alert" на form errors; coverage Application 67%→83%, Domain 11%→79%; property-based tests на StockService (Σ movements ≡ Stock); verified backup-recovery drill RTO ~25s. |
| **16** (regression) | Regression suite 35 Playwright flows + 60 visual snapshots; nightly stage-verify cron; Forgejo workflow regression; README badges; factories для test-data. |
| **17** (onboarding) | `/onboarding-wizard` (4 шага + skip + `localStorage.fm.wizardCompleted`); `HelpTooltip` + 13 topics; `/help` knowledge base из 7 markdown'ов; `FeedbackWidget` (bug/suggestion/question + Telegram fallback); `/admin/diagnostic` (7 параллельных проверок); `/whats-new` из `CHANGELOG.md`; `EmptyStateWithDemo`. |
| **18** (TODO cleanup) | P0 race в `GenerateNumberAsync` через PostgreSQL advisory lock (`pg_advisory_xact_lock(orgHash, docTypeHash)`); `WhatsNewBanner` в AppLayout; color contrast WCAG-AA (19 файлов); `useFormatCurrency()` hook; audit-log UI filters (Кто/Дата с/по); `NotificationCenter` (bell-icon SignalR-popover). |
| **19** (power UX) | Phase19a: Product.IsArchived + IsAvailableForSale (partial-index). `POST /api/catalog/products/bulk-update {ids, op, params}` — 5 операций (price-adjust %/абсолют, change-group, archive/unarchive, toggle-sale) одной транзакцией. `SavedPresets` chips (UserPreset jsonb). `QuickActionsPalette` (Cmd+J отдельно от Cmd+K). `InlinePriceCell` dblclick → input optimistic + revert. CSV import 1000 строк транзакцией. `ExportButton` (CSV/XLSX) на 5 контроллерах. Keyboard nav в DataTable (↑↓/Enter/Space/Delete). |
| **20** (Mapster + maintenance) | TD-3: `MapsterConfig.cs` + `.ProjectToType<TDto>(MapsterConfig.Config)` вместо ручных Select-expression'ов. SSO scaffold: `Microsoft.AspNetCore.Authentication.Google` + `.MicrosoftAccount` (conditional registration); `ExternalAuthController` (503 если не настроено, 501 callback с email для invite-flow). 3 новых cleanup-job'a (org-audit-log >90д, drafts >30д, refresh-tokens revoked >7д). `DatabaseMaintenanceJobs.VacuumTopTablesAsync` (топ-5 таблиц, weekly). `DiskMonitoringJob` ежечасно + Telegram-alert <1GB + Prom-gauge `food_market_disk_free_bytes{mount}`. `~/nightly-perf-check.sh` baseline-comparison через `/metrics`. Astro layout: gtag/Yandex.Metrika placeholders + `docs/analytics.md`. |
| **21** (prod toolchain) | `deploy/check-prod-readiness.sh` (backup<60min, disk≥5GB, /health, .env), `prod-deploy.sh` (blue-green :8088 + nginx upstream switch), `prod-rollback.sh` (atomic), `post-deploy-smoke.sh` (10 шагов JSON через python3 — на stage 10/10 ✓), `db-schema-diff.sh` (pg_dump через ssh+docker exec, sed-нормализация, diff -u), `generate-release-notes.sh` (git log → markdown group by prefix), `.forgejo/workflows/auto-tag.yml` (v<YYYYMMDD>.<N>). Все скрипты — `--dry-run`. |
| **22** (data tooling) | Phase22a: `org_exports` таблица (jsonb config-like, unique download token). `POST /api/org/export` → Hangfire `OrgExportJob` собирает ZIP с JSON-файлами по каждой сущности → IObjectStorage + DownloadToken 64-hex + 24h TTL + email-notify. `POST /api/catalog/products/import/1c-csv` (Windows-1251 + auto-detect разделитель + русские заголовки). `deploy/anonymize-prod.sh` (PII обфускация: email→user{N}@example.kz, phone→+7700111{N:04}, passwords→тестовый hash, BIN/IIN синтетические, MoySkladToken=NULL). `DbSchemaDocsJob` weekly → `db-schema-generated.md` с mermaid ER-диаграммой. `POST /api/admin/audit-log/export?format=csv|jsonl` streaming. `GET /api/moysklad/sync-status` агрегат last-success / last-7d errors / pending. **Итог: финальный ARCHITECTURE.md (этот).** |

## Production readiness (после 22 спринтов)

### Реализовано полностью

- Backend: auth (OpenIddict password+refresh+revoke), multi-tenant (query-filter + advisory locks), все 8 типов документов (Supply/Enter/Loss/Transfer/Inventory/RetailSale/Demand/SupplierReturn+CustomerReturn) с проводкой через Serializable transactions и ОФД-snapshot полями.
- Каталог: products + barcodes + prices + images (thumb/medium WebP), groups (иерархия с Path), counterparties, units, currencies, countries, stores, retail-points.
- Reports: Sales / Stock / Profit / ABC с CSV+XLSX export'ом, all multi-tenant.
- Background: Hangfire с 10 recurring jobs (housekeeping, email-notify, telegram-summary, vacuum, disk-monitor, db-schema-docs).
- Observability: Prometheus `/metrics` (HTTP + DB query duration + business counters + disk gauge), Serilog structured logging, /health/{live,ready} с DB-проверкой.
- A11y: WCAG 2 AA color contrast, focus-trap в modal, axe-core spec-suite 0 critical, keyboard-nav (Cmd+K, Cmd+J, table ↑↓/Enter/Space/Delete).
- Tests: 80%+ coverage на Application, integration tests с Testcontainers, e2e Playwright 44 specs зелёные на stage, k6 load baseline.
- Web: React 19 + Vite + TS, AG Grid Community, TanStack Query, 200 KB initial bundle (gzip), inline-edit, bulk-операции, CSV import/export, SavedPresets, Cmd+J QuickActions, NotificationCenter.
- POS: WPF на .NET 8, оффлайн-буфер SQLite, синк через `/api/pos/v1/*` с идемпотентным batch-ack, ОФД-провайдеры (Mock работает, реальные — scaffolding).
- DevOps: backup-таймер с retention 30d, stage→prod toolchain (7 скриптов из Sprint 21), auto-tag workflow, anonymize-prod для безопасных stage-дампов.

### Scaffolding (готово к подключению, но не активно)

| Что | Где | Что нужно от user'а |
|---|---|---|
| **SSO Google** | `Authentication:Google:ClientId/Secret` | OAuth credentials с Google Cloud Console |
| **SSO Microsoft** | `Authentication:Microsoft:ClientId/Secret` | OAuth credentials с Azure App Registration |
| **ОФД Webkassa** | `OrganizationFiscal.{Endpoint,Login,Password,CashboxId}` | Договор + кассовый аппарат + creds |
| **ОФД Kassa24 / ОФД-Соло** | то же | Договоры с провайдерами |
| **MoySklad sync** | `Organization.MoySkladToken` | Per-org OAuth token у клиента |
| **Telegram alerts** | `Monitoring:SuperAdminTelegramChatIds` | Chat-id'ы суперадминов |
| **Yandex.Metrika / GA4** | env `PUBLIC_YM_ID` / `PUBLIC_GA_ID` | Счётчики у клиента |
| **SMTP** | `PlatformSettings.Smtp*` | SendGrid / Mailgun / Yandex300 креды |
| **MinIO storage** | `Storage:Minio:Endpoint/AccessKey/SecretKey` | S3-совместимый bucket (опц.) |

### Не реализовано (требует отдельного решения)

- **Прод-деплой** — toolchain готов (`deploy/prod-deploy.sh`), но реальный сервер не настроен (DNS, certbot, /etc/nginx/conf.d/food-market-upstream.conf).
- **SSO callback flow** — `/api/auth/external/callback` возвращает 501 с email; нужен invite-flow + tokens-issuance.
- **Kazakh-перевод** — i18n keys на русском; для прод-релиза в РК нужен носитель языка.
- **POS Windows-тест** — POS-проект собирается на macOS/Linux но требует Windows для UI-тестов.
- **Down-миграции** — EF Migration.Down() есть в коде, но не валидированы для прод-данных (data loss risk).
- **Public marketing site SEO** — `food-market.kz` (Astro) собирается, но не задеплоен.

### Файловая структура (актуальная)

```
food-market/
├── src/
│   ├── food-market.domain/           # POCO + interfaces + enums
│   ├── food-market.application/      # DTO + handlers + mapping (Sprint 20)
│   ├── food-market.infrastructure/   # EF + Identity + OpenIddict + integrations
│   ├── food-market.api/              # Controllers + middleware + Hangfire jobs + storage
│   ├── food-market.web/              # React admin SPA
│   ├── food-market.public/           # Astro marketing site
│   ├── food-market.shared/           # POS↔API DTO-контракты
│   ├── food-market.pos.core/         # POS-логика (UI-agnostic)
│   └── food-market.pos/              # WPF (.NET 8 Windows)
├── tests/
│   ├── food-market.UnitTests/        # xUnit + InMemory EF
│   ├── food-market.IntegrationTests/ # xUnit + Testcontainers Postgres
│   ├── e2e/                          # Playwright + ad-hoc smoke scenarios
│   └── load/                         # k6 (retail-sales-parallel, signup-burst, …)
├── deploy/
│   ├── docker-compose.yml            # Postgres + api + web + (registry)
│   ├── Dockerfile.{api,web,public}
│   ├── nginx.conf                    # SPA + reverse-proxy
│   ├── backup.sh / food-market-backup.* # systemd-timer ежедневный бэкап
│   ├── check-prod-readiness.sh       # Sprint 21
│   ├── prod-deploy.sh                # Sprint 21
│   ├── prod-rollback.sh              # Sprint 21
│   ├── post-deploy-smoke.sh          # Sprint 21
│   ├── db-schema-diff.sh             # Sprint 21
│   ├── generate-release-notes.sh     # Sprint 21
│   └── anonymize-prod.sh             # Sprint 22
├── docs/                             # 30+ markdown файлов
├── .forgejo/workflows/               # CI (ci.yml, regression.yml, auto-tag.yml, …)
└── food-market.sln
```

## Релиз-цикл

1. Локально: `dotnet build` + `dotnet test` + `pnpm build`.
2. `git push origin main` (Forgejo на 127.0.0.1:3000 — primary remote,
   GitHub — mirror, memory `feedback_forgejo_primary`).
3. `~/deploy-stage.sh` — docker build api+web → push в локальный registry
   `192.168.1.193:5001` → ssh на prod-vm → `docker compose pull && up -d`.
4. Health check на `https://test.admin.food-market.kz/health/ready`.
5. Verify на stage (Playwright или ручной чек).
6. Prod-деплой — пока ручной (TBD, нужен план от user'а).

## Что ещё прочитать

- [MULTI-TENANCY.md](MULTI-TENANCY.md) — query filter, SuperAdmin override, подводные камни.
- [RUNBOOK.md](RUNBOOK.md) — операционные процедуры.
- [DEVELOPER-GUIDE.md](DEVELOPER-GUIDE.md) — как начать вкладываться в код.
- [ofd-integration.md](ofd-integration.md) — ОФД-провайдеры.
- [openapi.md](openapi.md) — генерация TS-клиента из Swagger.
- [observability.md](observability.md) — Serilog + Prometheus.
- [secrets.md](secrets.md) — управление секретами в stage/prod.
- [stage-access.md](stage-access.md) — как попасть на stage-сервер.
- [backup-restore.md](backup-restore.md) — бэкапы.
