# Архитектура 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 дней. - Ретрай отправки чеков в ОФД (когда появится интеграция). - Еженедельная инвентаризация напоминаний.