food-market/docs/ARCHITECTURE.md
nns 97e26a65d5 docs(s12): ARCHITECTURE/MULTI-TENANCY/RUNBOOK/DEVELOPER-GUIDE + k6 baseline + stage-verify CI
Документация для следующего разработчика (4 файла, ~1500 строк по
существу), реальный нагрузочный baseline на stage, и автоматический
smoke на каждый push.

Доки:
- docs/ARCHITECTURE.md — карта слоёв, модулей, Program.cs composition
  root, полный поток signup→post с трассировщиком ASP.NET pipeline.
- docs/MULTI-TENANCY.md — ITenantEntity + reflection query-filter,
  stamping в SaveChanges, SuperAdmin override (read-only + edit-mode
  с reason), 8 подводных камней, чеклист «как добавить tenant-сущность».
- docs/RUNBOOK.md — health-чеки, backup/restore с примером, смена SDK,
  disaster-recovery на новый сервер, 6 описанных инцидентов
  (включая docker-compose project name), БД-troubleshooting.
- docs/DEVELOPER-GUIDE.md — локальный setup, гочи integration-тестов,
  полные паттерны (controller с permission + tenant-сущность с
  RowVersion + 5 шагов миграции), валидация, structured-логирование,
  «НЕ делать» список.

k6 baseline:
- tests/load/ — 3 скрипта (signup-burst, retail-sales-parallel,
  sales-report-heavy) + README с инструкциями.
- docs/performance-baseline.md — реальные цифры на stage:
  * signup p95 446ms @ 50 RPM (IP-лимит 60/мин держит);
  * retail-sale sequential — 17/sec, p95 71ms;
  * retail-sale @ VU>1 — 53% failure из-за race в
    GenerateNumberAsync (unique-violation 23505 не ловится в
    SaveOrFkErrorAsync) — P0 для следующего рефакторинга;
  * reports на 1500 чеков — p95 50-114ms до VU=5.

CI:
- .forgejo/workflows/stage-verify.yml — on workflow_run после Docker
  API/Web, wait-for-ready → tests/stage-smoke.sh → Telegram пинг.
- tests/stage-smoke.sh — 7-секундный bash-смок (curl+jq+python3),
  5 этапов: health, signup, token, multi-tenant изоляция (B → 404
  на product A, B → пустой список), полный документ-цикл
  (supplier+supply.post → stock=100 → sale.post → stock=99).
  Локальный прогон против stage — все этапы зелёные.

Build чистый, локальный прогон smoke зелёный. Sprint 12 закрывает
автономно-безопасный цикл — дальше нужен вход от user'а.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 03:19:25 +05:00

26 KiB
Raw Blame History

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.csITenantEntity (обязательный 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/ITenantContextOrganizationId, 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).
  • 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.
  • 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:ProductsEditRolePermissions.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.csSELECT 1 + проверка __EFMigrationsHistory.
    • Security/OpenIddictKeyConfigurator.cs — в dev — persistent RSA в App_Data/oidc-keys/*; в stage/prod — X509 PFX из конфига (см. 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.

Релиз-цикл

  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'а).

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