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>
406 lines
27 KiB
Markdown
406 lines
27 KiB
Markdown
# 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) — бэкапы.
|