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

88 lines
6.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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