food-market/docs/architecture.md
nns fd2f5ae4f3 Phase 0: project scaffolding and end-to-end auth
- .NET 8 LTS solution with 7 projects (domain/application/infrastructure/api/shared/pos.core/pos[WPF])
- Central package management (Directory.Packages.props), .editorconfig, global.json pin to 8.0.417
- PostgreSQL 14 dev DB via existing brew service; food_market database created
- ASP.NET Identity + OpenIddict 5 (password + refresh token flows) with ephemeral dev keys
- EF Core 8 + Npgsql; multi-tenant query filter via reflection over ITenantEntity
- Initial migration: 13 tables (Identity + OpenIddict + organizations)
- AuthorizationController implements /connect/token; seeders create demo org + admin
- Protected /api/me endpoint returns current user + org claims
- React 19 + Vite 8 + Tailwind v4 SPA with TanStack Query, React Router 7
- Login flow with dev-admin placeholder, bearer interceptor + refresh token fallback
- docs/architecture.md, CLAUDE.md, README.md

Verified end-to-end: health check, password grant issues JWT with org_id,
web app builds successfully (310 kB gzipped).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:59:13 +05:00

6.2 KiB
Raw Blame History

Архитектура food-market

Общая схема

┌─────────────────────────────────────────────────────────┐
│                  Internet / LAN магазина                 │
└─────────┬────────────────────────┬──────────────────────┘
          │                        │
          │ HTTPS                   │ HTTPS (+ офлайн-буфер)
          ▼                        ▼
┌───────────────────┐     ┌──────────────────────┐
│  web admin (React) │     │  Windows POS (WPF)   │
│   — браузер         │     │   — с локальной БД    │
│                    │     │   — работает с весами │
└─────────┬──────────┘     └──────────┬───────────┘
          │                           │
          └─────────────┬─────────────┘
                        ▼
          ┌──────────────────────────────┐
          │     food-market.api          │
          │  ASP.NET Core + OpenIddict   │
          │  SignalR hubs для sync       │
          └──────────┬───────────────────┘
                     │
         ┌───────────┼──────────┐
         ▼           ▼          ▼
   ┌─────────┐ ┌──────────┐ ┌──────────┐
   │Postgres │ │ Hangfire │ │  Logs    │
   │  16     │ │ (jobs)   │ │ Serilog  │
   └─────────┘ └──────────┘ └──────────┘

Слои сервера (Clean Architecture)

  1. Domain — сущности, value objects, события домена. Не зависит ни от чего.
  2. Application — use cases (MediatR handlers), DTO, интерфейсы (репозитории, внешние сервисы). Зависит только от Domain + Shared.
  3. Infrastructure — EF Core DbContext, репозитории, Identity, OpenIddict EF store, HTTP-клиенты к внешним сервисам (ОФД, SMS и т.д.). Зависит от Application + Domain.
  4. Api — ASP.NET Core host: controllers, middleware, DI wiring, хостинг. Зависит от всех выше.

Мультитенантность

Модель данных

  • Organization — корневая сущность тенанта. Сама НЕ tenant-scoped.
  • Любая другая доменная сущность реализует ITenantEntity (поле OrganizationId).
  • User.OrganizationId — к какой организации принадлежит пользователь.

Изоляция

  • При каждом запросе HttpContextTenantContext извлекает org_id из JWT.
  • AppDbContext.OnModelCreating проходит по всем Entity Types и для каждого ITenantEntity добавляет HasQueryFilter через reflection.
  • В итоге _db.Products.ToListAsync() вернёт только товары текущей организации.
  • Роль SuperAdmin обходит фильтр (для техподдержки).

Стамповка

  • При SaveChanges если добавляется новая ITenantEntity и её OrganizationId == Empty, она автоматически получает OrganizationId из текущего tenant context.
  • То же с timestamps: CreatedAt при add, UpdatedAt при modify.

Аутентификация

  • OpenIddict 5 в роли OAuth2/OIDC сервера.
  • Password Flow для первичного логина (логин/пароль → токены).
  • Refresh Token Flow для продления сессии.
  • В dev подписи/шифрование — ephemeral ключи; в prod — настоящие X.509 сертификаты.
  • Access token содержит claims: sub (user id), name, email, role, org_id.
  • Токен валидируется через AddValidation().UseLocalServer() (без дополнительного запроса к IdP).

Синхронизация с POS

Паттерн: Pull-sync + incremental

  1. POS периодически (каждые N сек/мин + по событию) запрашивает у сервера delta: GET /api/pos/sync?since={lastSyncTs}.
  2. Сервер возвращает JSON с изменениями справочников (товары, цены, сотрудники, настройки) за период.
  3. POS применяет изменения в локальной SQLite БД в одной транзакции.
  4. Продажи, совершённые на POS, пушатся обратно: POST /api/pos/sales с массивом документов.
  5. Если связи нет — POS продолжает работать, копит изменения в локальной очереди, отправляет при восстановлении.

Conflict resolution

  • Сервер — источник правды для справочников.
  • POS — источник правды для продаж на своей кассе (каждая продажа имеет posTerminalId + локальный externalId, которые серверу гарантируют идемпотентность).

Фоновые задачи

Hangfire + PostgreSQL storage:

  • Агрегация дневной выручки для dashboard.
  • Очистка логов старше 90 дней.
  • Ретрай отправки чеков в ОФД (когда появится интеграция).
  • Еженедельная инвентаризация напоминаний.