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