food-market/docs/ARCHITECTURE.md
nns aa83f82dc5
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
feat(s22): data tooling — export/import + schema docs + anon dump (7 пунктов)
1. GDPR org export — domain OrgExport + Phase22a миграция, OrgExportJob
   собирает ZIP с JSON по каждой сущности через IObjectStorage,
   DownloadToken 64-hex + 24h TTL + email-notify.
   POST /api/org/export, GET /api/org/export[/{id}], GET download/{token}.

2. 1C CSV import — POST /api/catalog/products/import/1c-csv:
   Windows-1251/UTF-8 BOM auto-detect, разделитель ;/, русские заголовки
   (Артикул/Наименование/Единица/Цена/Группа/Штрихкод) или английские.
   Нормализация unit-кодов (шт/кг/г/л/мл/упак). Делегирует на ImportCsv
   (транзакция, multi-tenant). docs/imports.md.

3. deploy/anonymize-prod.sh — pg_dump прода → restore во временную БД →
   UPDATE PII (email→user{N}@example.kz, phone→+7700111{N:04}, password→
   тестовый hash, BIN/IIN синтетические, MoySkladToken=NULL, аудиты
   TRUNCATE) → pg_dump → gz файл.

4. DbSchemaDocsJob (weekly вс 05:00 UTC) — information_schema → md с
   таблицами + колонками + FK + mermaid ER-диаграммой (топ-20 таблиц).
   Сохраняет в content-root db-schema-generated.md.

5. POST /api/admin/audit-log/export?format=csv|jsonl — streaming через
   AsAsyncEnumerable. UTF-8 BOM для CSV, JSONL для grep'a. Multi-tenant.

6. GET /api/moysklad/sync-status — агрегат по import_jobs:
   { configured, lastSuccessAt, errorCountLast7Days, pendingCount,
     byKind: { products: KindStatus, counterparties: KindStatus } }.
   Stub если MoySkladToken=null.

7. docs/ARCHITECTURE.md — финальный итог 22 спринтов:
   - Sprint 13-22 changes-сводка
   - «Реализовано полностью» секция
   - «Scaffolding» таблица с указанием что нужно от user'а
   - «Не реализовано» секция (прод, SSO callback, KZ-перевод, POS-тест)
   - Актуальная файловая структура

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

487 lines
37 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-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, disk5GB, /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) — бэкапы.