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

398 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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