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>
37 KiB
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— оптимистичная блокировка через PGxmin(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).Persistence/Configurations/*.cs— EF Core fluent configs по поддоменам.Persistence/Migrations/— миграции пишутся вручную (см. CLAUDE.md / memoryfeedback_ef_migrations), снапшот не синхронизируется с моделью (используется толькоdotnet ef migrations add, который не вызывается в этом проекте).Persistence/OrgAuditInterceptor.cs— EFISaveChangesInterceptor, пишет каждую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.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+PermissionAuthorizationPolicyProviderPermissionAuthorizationHandler— permission-based авторизация.[RequiresPermission("ProductsEdit")]→ policyperm: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в SerilogLogContext, каждая запись в журнале получает эти лейблы.Observability/DbMetricsInterceptor.cs— EF интерсептор, Prometheusfood_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).
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— регистрирует clientfood-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)
Логические блоки в порядке регистрации:
- Serilog bootstrap (до builder).
- CORS (
Cors:AllowedOriginsиз конфига). - HttpContextAccessor +
ITenantContext+IClaimsTransformation(SuperAdmin override роли). - EF Core:
AppDbContext(Npgsql, OpenIddict, два interceptor'а). - Identity + OpenIddict server (password + refresh, rolling refresh, leeway = 0).
- Authentication/Authorization policies (
AdminAccess,perm:*). - Rate-limiter (
/connect/token,/api/auth/signup). - HealthChecks (
databaseтегready). - IEmailSender (Singleton + scope для DbContext).
- IFiscalProvider + 3 HttpClient +
IFiscalProviderFactory. - MediatR (assembly scan), FluentValidation.
- MoySklad HttpClient + import service.
- Hangfire server + storage (PG),
HangfireJobsConfiguratorхостед. - SignalR +
INotificationsPublisher. - Telegram-бот HttpClient (если token задан).
- Сидеры:
OpenIddictClientSeeder,SystemReferenceSeeder,DevDataSeederхостед;DemoTenantSeeder/YearDemoSeederscoped. Build()→ middleware pipeline (Serilog→CORS→HttpMetrics→ RateLimiter→hubs-token-fix→AuthN→AuthZ→LogEnrichment→ ReadonlyOverride→StaticFiles[/uploads]→Swagger→MapControllers→ MapHub→MapMetrics→HangfireDashboard→HealthChecks).- На старте:
db.Database.Migrate()(идемпотентно). 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>. SharedApiFactoryчерезApiCollection(один контейнер на сессию xunit). Memory note:test_suites_setup— Ryuk выключен (TCN не тянет с docker-hub), rate-limiter eager-config через env-переменную. - E2E (
tests/e2e/) — Playwright (TS) против stagehttps://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.). Все скрипты — --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 |
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
Релиз-цикл
- Локально:
dotnet build+dotnet test+pnpm build. git push origin main(Forgejo на 127.0.0.1:3000 — primary remote, GitHub — mirror, memoryfeedback_forgejo_primary).~/deploy-stage.sh— docker build api+web → push в локальный registry192.168.1.193:5001→ ssh на prod-vm →docker compose pull && up -d.- Health check на
https://test.admin.food-market.kz/health/ready. - Verify на stage (Playwright или ручной чек).
- Prod-деплой — пока ручной (TBD, нужен план от user'а).
Что ещё прочитать
- MULTI-TENANCY.md — query filter, SuperAdmin override, подводные камни.
- RUNBOOK.md — операционные процедуры.
- DEVELOPER-GUIDE.md — как начать вкладываться в код.
- ofd-integration.md — ОФД-провайдеры.
- openapi.md — генерация TS-клиента из Swagger.
- observability.md — Serilog + Prometheus.
- secrets.md — управление секретами в stage/prod.
- stage-access.md — как попасть на stage-сервер.
- backup-restore.md — бэкапы.