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
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>
487 lines
37 KiB
Markdown
487 lines
37 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-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, disk≥5GB, /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) — бэкапы.
|