food-market/docs/ARCHITECTURE.md
nns 9588d03bf4 test(s15): axe a11y + focus traps + unit coverage 80% + property tests + backup drill
Sprint 15 финальный — реальные axe + coverage + pg_restore numbers.

Ключевые цифры:
- axe-core: critical=0 on 10 страниц stage'а; serious 12→9
  после фиксов (sidebar contrast + 8 icon-only back-arrow aria-labels).
- Unit coverage: Application 56%→83%, Domain 11%→79%, combined
  60%→80%. Тестов 68→147 (+79).
- Backup recovery drill: RTO ~25 секунд end-to-end
  (pg_dump 2s + pg_restore 4s + dotnet startup 19s).

Что сделано:
1. @axe-core/playwright + stage-ui-15 (10 страниц) + stage-ui-16
   (SR smoke на login: getByLabel, role=alert, aria-describedby,
   keyboard nav).
2. useFocusTrap hook (WCAG 2.4.3 + 2.1.2): return-focus, mount-focus,
   Tab cycle. Подключён к Modal + ConfirmDialog с opt-in
   defaultFocus='cancel'|'confirm'. ConfirmDialog по дефолту фокусит
   Cancel для destructive actions (safer чем Enter→Delete).
3. A11y фиксы:
   • text-slate-400→text-slate-500 в sidebar (contrast 2.63→4.61).
   • 8 страниц edit с back-arrow Link — aria-label + aria-hidden
     на иконке + текст-slate-500 цвет.
   • Modal close button — то же.
   • LoginPage — aria-invalid/aria-describedby/role=alert на
     ошибках валидации.
   • Field component — role="alert" на error span (announce'ит SR).
4. 8 файлов unit-тестов: PhoneNormalization, PagedRequest,
   RequiredGuid, RolePermissions (Domain), DomainPocoSmoke,
   DomainFullPropertyTouch, CatalogDtosSmoke, StockServiceProperty
   (4 seeds × 4 size + batch + 2-product isolation).
5. Backup-drill: pg_dump со stage'а → fresh postgres:16-alpine →
   pg_restore → dotnet run против восстановленной БД → /health/ready
   Healthy. Команды и timing в RUNBOOK.md.
6. Docs review:
   • MULTI-TENANCY чеклист «добавить tenant-сущность» расширен с 6
     до 19 шагов (Domain → EF Config → Migration с Xmin →
     RolePermissions → Validation → Controller + RequiresPermission →
     Audit + SensitiveOpsAudit → property tests).
   • ARCHITECTURE.md — Sprint 13-15 changes таблица.
   • DEVELOPER-GUIDE.md — «что добавилось после первого guide'а» +
     a11y pitfalls в «что НЕ делать».

Stage smoke ✓. Это финальный автономно-безопасный спринт. Дальше
нужен вход от user'а (ОФД keys, MoySklad tokens, Windows для POS,
прод-деплой план, kz-перевод, реальный SMTP).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 14:53:38 +05:00

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