# 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()`. ### 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`. 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`. ## Релиз-цикл 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) — бэкапы.