docs: reorganize system knowledge by domain
This commit is contained in:
+21
-7
@@ -1,9 +1,23 @@
|
||||
# Архитектура
|
||||
# Архитектура WealthLedger
|
||||
|
||||
Здесь описывается фактическое устройство WealthLedger и границы компонентов.
|
||||
Архитектурное описание объясняет границы приложений, владение данными и
|
||||
взаимодействия. Для знакомства с пользовательскими задачами начните с
|
||||
[обзора продукта](../overview.md), затем прочитайте Context и Container.
|
||||
|
||||
- [Обзор системы](system-overview.md) — поставленные возможности, локальные
|
||||
границы и основные потоки данных.
|
||||
- [HTTP API и ошибки](http-api.md) — validation, error envelope и границы доказательства.
|
||||
- [Глоссарий домена](domain-glossary.md) — канонические значения терминов.
|
||||
- [ADR](../adr/README.md) — долгоживущие решения и их последствия.
|
||||
| Вопрос | Где искать |
|
||||
| --- | --- |
|
||||
| Кто пользуется системой и какие поставщики находятся снаружи? | [C4 System Context](context.md), [внешние интеграции](../integrations/README.md) |
|
||||
| Из каких приложений и хранилищ состоит система? | [C4 Container](containers.md) |
|
||||
| Как разделены данные, полномочия и secrets? | [Хранение и доступ](containers.md#хранимые-данные-и-границы-доступа), [аутентификация](../domains/authentication/overview.md), [подключение брокера](../integrations/tbank.md) |
|
||||
| На каких решениях основано устройство системы? | [Стратегия](containers.md#стратегия-решения-и-её-основания), [ADR](../adr/README.md) |
|
||||
| Что ограничивает точность, свежесть и эксплуатацию? | [Качество и ограничения](containers.md#качество-и-ограничения) |
|
||||
| Как взаимодействуют части во время запроса? | [Загрузка портфеля](../domains/portfolio/flows/portfolio-loading.md), [восстановление сессии](../domains/authentication/overview.md#восстановление-сессии), [сохранение подключения](../integrations/tbank.md#проверка-и-сохранение-подключения) |
|
||||
| Что считается кэшем и когда прежние данные теряют право на повторное использование? | [Свежесть данных](cross-cutting/data-freshness.md) |
|
||||
| Как обрабатываются ошибки и ожидание? | [HTTP API и ошибки](http-api.md), [состояния интерфейса](cross-cutting/ui-resilience.md) |
|
||||
| Как связать ошибку с обработкой запроса? | [Наблюдаемость](http-api.md#наблюдаемость) |
|
||||
| Как устроено подтверждённое локальное окружение? | [Container](containers.md#представление-container), [локальная диагностика](../development/local-workspace.md) |
|
||||
| Что означают предметные термины? | [Глоссарий](../glossary.md) |
|
||||
|
||||
Эта карта покрывает вопросы архитектуры там, где о них есть подтверждённое
|
||||
содержание. Правила размещения и изменения кода находятся в локальных
|
||||
инструкциях frontend/backend; системные объяснения остаются здесь.
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
# Приложения, хранилище и взаимодействия
|
||||
|
||||
Представление C4 Container раскрывает внутреннее устройство WealthLedger.
|
||||
Контейнер здесь означает приложение или хранилище, а не Docker-контейнер.
|
||||
Внешние системы совпадают с [System Context](context.md).
|
||||
|
||||
## Представление Container
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
U["Частный инвестор<br/>Человек"]
|
||||
subgraph W["WealthLedger · программная система"]
|
||||
F["React SPA<br/>TypeScript, Vite, Mantine, TanStack Query"]
|
||||
B["Backend API<br/>NestJS, Node.js"]
|
||||
D[("Локальная база<br/>Embedded SQLite")]
|
||||
F -->|"HTTP JSON /api/v1; Bearer и auth cookie"| B
|
||||
B -->|"Prisma + better-sqlite3; доступ к локальному файлу"| D
|
||||
end
|
||||
T["T-Bank Invest API<br/>Внешняя система"]
|
||||
M["MOEX ISS<br/>Внешняя система"]
|
||||
N["Реестр НКР<br/>Внешняя система"]
|
||||
C["T-Bank Brands CDN<br/>Внешняя система"]
|
||||
U -->|"Просматривает данные и управляет подключением"| F
|
||||
B -->|"gRPC over TLS; token пользователя"| T
|
||||
B -->|"HTTPS; публичные данные"| M
|
||||
B -->|"HTTPS; публичный HTML"| N
|
||||
F -->|"HTTPS; изображения без авторизации"| C
|
||||
```
|
||||
|
||||
SPA выполняется в браузере пользователя. Backend — отдельное локальное
|
||||
приложение; SQLite используется внутри его процесса через библиотеку и файл
|
||||
базы. Между API и SQLite нет сетевого database server. Граница системы на
|
||||
схеме показывает принадлежность частей, а не их размещение на одном узле.
|
||||
|
||||
## Ответственность частей
|
||||
|
||||
| Часть | Ответственность |
|
||||
| --- | --- |
|
||||
| React SPA | Экраны, навигация, формы, состояние входа и клиентский кэш ответов; форматирование денежных значений |
|
||||
| Backend API | Локальная идентичность, сессии, проверка доступа, подключение брокера, нормализация внешних данных, точные расчёты и серверные кэши |
|
||||
| SQLite | Долговременное хранение локальных пользователей, сессий и зашифрованных подключений |
|
||||
| Внешние системы | Данные брокера, публичные сведения об инструментах и изображения в пределах своей ответственности |
|
||||
|
||||
Frontend организован слоями `app → pages → features → entities → shared`:
|
||||
каркас связывает маршруты, страницы собирают сценарии, а общие средства
|
||||
обслуживают транспорт и представление. Backend остаётся модульным NestJS API,
|
||||
где локальная аутентификация отделена от подключения брокера и инвестиционных
|
||||
сценариев. Это внутренняя организация приложений, не набор микросервисов.
|
||||
|
||||
Финансовые HTTP-запросы браузера идут через backend. Пользователь вводит token
|
||||
T-Bank в форме и отправляет его для сохранения, но API не возвращает token
|
||||
в ответах; SPA не использует его для прямых брокерских запросов. Прямая
|
||||
загрузка публичного изображения из CDN не проходит через эту финансовую
|
||||
границу.
|
||||
|
||||
Типизированный frontend-клиент производится из OpenAPI backend во время
|
||||
разработки. В runtime SPA вызывает API, а не генератор или OpenAPI endpoint.
|
||||
Форма запросов и ошибок описана в [HTTP API](http-api.md); направление
|
||||
генерации принято в [ADR 002](../adr/002-nest-generated-openapi-contract.md).
|
||||
|
||||
## Хранимые данные и границы доступа
|
||||
|
||||
SQLite содержит пользователя с нормализованным email и hash пароля, его
|
||||
сессии с hash текущего refresh token, сроком действия и признаком отзыва,
|
||||
а также одно зашифрованное подключение T-Bank с временем подключения.
|
||||
Пароли хешируются bcrypt; token брокера шифруется AES-256-GCM. Plaintext
|
||||
credentials не являются сохраняемым представлением этих данных.
|
||||
|
||||
Сессии и подключение связаны с владельцем. Access token подтверждает
|
||||
локального пользователя, но `accountId` из URL сам по себе не даёт права
|
||||
читать брокерский счёт. Backend получает текущее подключение пользователя
|
||||
и его generation перед использованием финансового кэша. Проверка доступности
|
||||
счёта у брокера зависит от сценария: журнал выполняет её до чтения страницы
|
||||
кэша; свежий портфель одного счёта может быть возвращён без нового запроса
|
||||
списка счетов. При обновлении портфеля доступ проверяется у брокера.
|
||||
|
||||
Ротация сессии атомарно заменяет hash текущего refresh token: два
|
||||
конкурентных запроса не могут оба успешно выполнить одну ротацию.
|
||||
Вызов брокера и запись в локальную базу не образуют общую транзакцию:
|
||||
при сохранении подключения сначала проверяется token, затем записывается
|
||||
шифротекст. Подробности находятся в
|
||||
[аутентификации](../domains/authentication/overview.md) и
|
||||
[интеграции T-Bank](../integrations/tbank.md).
|
||||
|
||||
Брокерские счета, сводки, портфель, страницы журнала и будущие события
|
||||
не являются долговременной историей в SQLite. Они получаются для текущего
|
||||
подключения и могут временно кэшироваться. Перезапуск backend не
|
||||
восстанавливает прежние финансовые снимки и cursor continuations из базы;
|
||||
[модель свежести](cross-cutting/data-freshness.md) отделяет это от хранения
|
||||
учётных данных.
|
||||
|
||||
## Стратегия решения и её основания
|
||||
|
||||
Локальная база и Prisma дают типизированный доступ к локальному состоянию
|
||||
без отдельного сервера базы; исходные причины и пределы выбора сохранены
|
||||
в [ADR 001](../adr/001-prisma-sqlite-local-mvp.md). Централизованная
|
||||
[серверная граница T-Bank](../adr/003-tbank-server-side-grpc-boundary.md)
|
||||
сосредоточивает credentials, лимиты и повторы вне браузера, а
|
||||
[шифрование подключения](../adr/004-encrypted-per-user-tbank-token.md)
|
||||
задаёт способ хранения брокерского секрета.
|
||||
|
||||
[Политика сессий](../adr/006-auth-session-policy.md) разделяет короткоживущий
|
||||
access token и вращаемый refresh token. [Политика query-кэша](../adr/007-react-query-fetch-policy.md)
|
||||
сохраняет доступные данные при обновлении и не запускает refetch при возврате
|
||||
фокуса. [Ограниченные повторы T-Bank](../adr/005-tbank-upstream-resilience.md)
|
||||
и [структурированные журналы](../adr/008-pino-backend-logging.md) обслуживают
|
||||
внешнюю границу. ADR сохраняют контекст принятия решения; текущие детали
|
||||
уточняются в тематических описаниях.
|
||||
|
||||
## Качество и ограничения
|
||||
|
||||
Точность представления денег требует сохранять валюту и decimal-значение;
|
||||
неизвестная сумма не заменяется нулём. Свежесть наблюдения не равна
|
||||
актуальности рынка в реальном времени. Временные ошибки внешнего поставщика
|
||||
обрабатываются ограниченными повторами и локальными состояниями интерфейса,
|
||||
без обещания успешного ответа при недоступном upstream.
|
||||
|
||||
Авторизация доступа, недопущение token в публичный ответ и атомарность
|
||||
ротации сессии — отдельные свойства; наличие типизированного клиента
|
||||
не доказывает их. Известные расхождения регистрации и деталей валидации
|
||||
отмечены в [аутентификации](../domains/authentication/overview.md#регистрация-и-вход)
|
||||
и [HTTP API](http-api.md#error-contract).
|
||||
|
||||
Подтверждённая топология — локальная SPA и локальный API с файлом SQLite.
|
||||
Origin браузера, адрес API и cookie policy должны быть согласованы; условия
|
||||
и диагностика описаны в [локальном окружении](../development/local-workspace.md).
|
||||
Наличие локального запуска не задаёт production-топологию или гарантии SLA.
|
||||
|
||||
Кэши и лимитер T-Bank действуют на экземпляр backend. Ротация ключа шифрования
|
||||
подключений и согласование изменений подключения между распределёнными
|
||||
экземплярами не входят в реализованный механизм. Эти ограничения не
|
||||
позволяют переносить локальную схему на несколько экземпляров без отдельного
|
||||
решения о состоянии, credentials и лимитах.
|
||||
|
||||
Runtime-сценарии раскрываются у соответствующих владельцев:
|
||||
[восстановление сессии](../domains/authentication/overview.md#восстановление-сессии),
|
||||
[сохранение подключения](../integrations/tbank.md#проверка-и-сохранение-подключения)
|
||||
и [загрузка портфеля](../domains/portfolio/flows/portfolio-loading.md).
|
||||
@@ -0,0 +1,42 @@
|
||||
# Контекст WealthLedger
|
||||
|
||||
Представление C4 System Context показывает WealthLedger целиком: кому служит
|
||||
приложение и от каких внешних систем получает сведения. Внутреннее устройство
|
||||
раскрыто отдельно в [Container](containers.md).
|
||||
|
||||
## Система и её окружение
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
U["Частный инвестор<br/>Человек"]
|
||||
W["WealthLedger<br/>Программная система"]
|
||||
T["T-Bank Invest API<br/>Внешняя система"]
|
||||
M["MOEX ISS<br/>Внешняя система"]
|
||||
N["Реестр НКР<br/>Внешняя система"]
|
||||
C["T-Bank Brands CDN<br/>Внешняя система"]
|
||||
U -->|"Подключает брокера и изучает инвестиции"| W
|
||||
W -->|"Получает доступные счета, позиции, операции и рыночные сведения"| T
|
||||
W -->|"Получает публичные данные облигаций и событий"| M
|
||||
W -->|"Получает кредитные рейтинги выпусков"| N
|
||||
W -->|"Загружает изображения инструментов"| C
|
||||
```
|
||||
|
||||
Частный инвестор входит в локальную учётную запись WealthLedger и добавляет
|
||||
своё подключение к брокеру. Отдельные роли администратора или брокера,
|
||||
работающего внутри WealthLedger, для описанных пользовательских сценариев
|
||||
не требуются.
|
||||
|
||||
T-Bank определяет доступность счетов и поставляет личные финансовые сведения.
|
||||
WealthLedger нормализует их для просмотра и сопоставляет с публичной
|
||||
информацией об инструментах. [Интеграция T-Bank](../integrations/tbank.md)
|
||||
объясняет полномочия подключения и ограничения внешней зависимости.
|
||||
|
||||
[MOEX ISS](../integrations/moex.md) поставляет публичные параметры и календарь выпусков облигаций.
|
||||
[Реестр НКР](../integrations/nkr.md) используется для поиска кредитного рейтинга конкретного выпуска;
|
||||
рейтинг эмитента не подставляется вместо него. Отсутствие подходящей записи
|
||||
и недоступность источника имеют разный смысл. Правила интерпретации этих
|
||||
сведений находятся в [описании облигаций](../domains/instruments/bonds.md).
|
||||
Публичные данные выпуска сами по себе не подтверждают личную выплату.
|
||||
|
||||
[T-Bank Brands CDN](../integrations/tbank.md#публичные-логотипы) поставляет только изображения для представления
|
||||
инструментов. Отсутствующий логотип не меняет финансовые данные.
|
||||
@@ -0,0 +1,72 @@
|
||||
# Свежесть данных и повторное использование
|
||||
|
||||
WealthLedger показывает финансовое наблюдение на момент получения данных.
|
||||
Временный кэш уменьшает повторные обращения к брокеру, но не превращает
|
||||
снимки в историю портфеля и не обещает рыночные данные в реальном времени.
|
||||
|
||||
## Клиентский и серверный снимки
|
||||
|
||||
[Client Cached Snapshot](../../glossary.md#client-cached-snapshot) — последний
|
||||
успешный ответ, сохранённый TanStack Query в SPA. При обычном фоновом обновлении
|
||||
того же набора данных уже показанный результат остаётся на экране; ошибка
|
||||
обновления сообщает о проблеме, не заменяя известные значения пустотой.
|
||||
Клиентский кэш не является внешней системой или отдельным runtime-приложением.
|
||||
|
||||
[Portfolio Snapshot](../../glossary.md#portfolio-snapshot) — полный результат
|
||||
загрузки портфеля на backend. В пределах разрешённого сценарием срока он может
|
||||
служить серверным fallback при временной ошибке upstream. Backend отмечает
|
||||
такой ответ `isStale`; frontend не выводит это поле из состояния собственного
|
||||
query-кэша. Серверное предупреждение и ошибка клиентского refetch могут
|
||||
возникать независимо.
|
||||
|
||||
Свежесть и допустимость fallback определяются сценарием. Портфельный fallback
|
||||
не распространяется автоматически на журнал, прогноз событий или страницу
|
||||
инструмента. Точные правила находятся в
|
||||
[справочнике портфеля](../../domains/portfolio/data-reference.md),
|
||||
[журнале операций](../../domains/operations/overview.md) и
|
||||
[будущих событиях](../../domains/income/upcoming-events.md).
|
||||
|
||||
По [ADR 007](../../adr/007-react-query-fetch-policy.md) запрос связан с открытием
|
||||
экрана, а возврат фокуса окна не запускает refetch. Повторное открытие после
|
||||
`staleTime` может обновить ответ в фоне. Эта политика не является таймером
|
||||
периодического обновления. Перезапуск backend уничтожает его in-memory
|
||||
снимки и continuations журнала; SQLite не восстанавливает их.
|
||||
|
||||
## Смена подключения
|
||||
|
||||
Кэш финансовых данных относится к пользователю, счёту и текущему
|
||||
подключению T-Bank. Backend сначала читает подключение и его generation, затем
|
||||
проверяет возможность повторного использования ответа. Свежий портфель
|
||||
одного счёта не требует нового запроса списка счетов у брокера; при обновлении
|
||||
портфеля доступ проверяется заново. Журнал операций проверяет доступ до чтения
|
||||
кэшированной страницы. Идентификатор счёта и совпадение параметров запроса
|
||||
сами по себе недостаточны для выдачи кэша.
|
||||
|
||||
При каждом успешном сохранении подключения token шифруется с новым
|
||||
AES-GCM nonce. Из сохранённого шифротекста backend вычисляет непрозрачную
|
||||
внутреннюю generation. Она не хранится отдельным полем базы и не входит
|
||||
в API, журналы или ошибки. Portfolio Snapshot и Ledger Page Cache допускают
|
||||
повторное использование только в пределах соответствующей generation.
|
||||
Старый кэш или поздно завершившийся запрос прежней generation не обслуживает
|
||||
новый запрос после замены подключения, включая сохранение того же plaintext
|
||||
token. Удаление подключения не оставляет доступного контекста для чтения кэша.
|
||||
|
||||
После успешного изменения подключения frontend сначала записывает
|
||||
подтверждённый status, затем сбрасывает generated GET queries в поддеревьях
|
||||
`/api/v1/brokerage-accounts`, `/api/v1/stocks` и `/api/v1/bonds`. Активные
|
||||
выборки запрашиваются заново, неактивные — при следующем использовании.
|
||||
Client Cached Snapshot прежнего подключения больше не служит результатом
|
||||
новой границы.
|
||||
|
||||
Неуспешная операция подключения не меняет статус, generation и доступные
|
||||
финансовые данные. Обычная ошибка refetch также сохраняет клиентский снимок
|
||||
текущей границы. Logout — отдельное событие: оно очищает состояние
|
||||
пользовательской сессии, status подключения и query cache.
|
||||
|
||||
Пользовательский сценарий описан в [интеграции T-Bank](../../integrations/tbank.md).
|
||||
Согласованные правила frontend находятся в workspace-спецификациях
|
||||
[query coherence](../../../specs/openspec/specs/frontend-query-coherence/spec.md)
|
||||
и [data resilience](../../../specs/openspec/specs/frontend-data-resilience/spec.md);
|
||||
серверные правила — в
|
||||
[portfolio](../../../specs/openspec/specs/portfolio-view/spec.md) и
|
||||
[ledger](../../../specs/openspec/specs/brokerage-account-ledger/spec.md).
|
||||
@@ -0,0 +1,49 @@
|
||||
# Ожидание, ошибки и восстановление интерфейса
|
||||
|
||||
Интерфейс различает ожидание данных, известную ошибку запроса и непойманную
|
||||
ошибку отображения маршрута. Эти состояния требуют разных действий и
|
||||
не подменяют друг друга.
|
||||
|
||||
## Ожидание и доступные данные
|
||||
|
||||
При первой загрузке без результата structural skeleton показывает
|
||||
расположение ожидаемых блоков. При частичной загрузке он занимает только
|
||||
незагруженную область. Placeholders не содержат правдоподобных финансовых
|
||||
данных, не становятся действиями и не получают клавиатурный фокус; область
|
||||
ожидания обозначается как занятая.
|
||||
|
||||
При восстановлении сессии SPA показывает нейтральный skeleton каркаса,
|
||||
пока неизвестно, доступен public или protected маршрут. При загрузке
|
||||
отложенного маршрута уже доступный каркас остаётся видимым.
|
||||
|
||||
Фоновое обновление и действие пользователя сохраняют видимые данные или
|
||||
элемент управления с компактной индикацией процесса. В частности,
|
||||
[клиентский снимок](data-freshness.md#клиентский-и-серверный-снимки) счетов или
|
||||
портфеля остаётся доступным после ошибки refetch; сообщение и повтор
|
||||
относятся к обновлению, а не к отсутствию инвестиций.
|
||||
|
||||
## Известная ошибка сценария
|
||||
|
||||
Ошибки счетов, портфеля и подключения T-Bank остаются локальными: рядом
|
||||
с затронутыми данными доступны предусмотренные сценарием retry и navigation
|
||||
actions. Устаревший серверный Portfolio Snapshot показывает сохранённый
|
||||
портфель с предупреждением. Он не превращается в глобальный fallback.
|
||||
Ошибка источника не означает нулевой баланс или отсутствие операций.
|
||||
|
||||
## Непойманная ошибка маршрута
|
||||
|
||||
Непойманная runtime-ошибка при отображении public или protected route
|
||||
заменяет его содержимое нейтральным fallback без технических деталей.
|
||||
Пользователь может перейти к `/accounts` при активной сессии или к `/login`
|
||||
без неё. Переход на другой маршрут сбрасывает это состояние; если новый
|
||||
маршрут отображается успешно, пользователь продолжает работу.
|
||||
|
||||
Общий fallback восстанавливает навигацию, но не обещает устранить причину
|
||||
ошибки. HTTP-коды и безопасная форма серверной ошибки находятся в
|
||||
[HTTP API](../http-api.md), восстановление сессии — в
|
||||
[аутентификации](../../domains/authentication/overview.md).
|
||||
|
||||
Согласованное поведение раскрыто в workspace-спецификациях
|
||||
[UI resilience](../../../specs/openspec/specs/frontend-ui-resilience/spec.md),
|
||||
[loading presentation](../../../specs/openspec/specs/frontend-loading-presentation/spec.md)
|
||||
и [data resilience](../../../specs/openspec/specs/frontend-data-resilience/spec.md).
|
||||
@@ -1,210 +0,0 @@
|
||||
# Глоссарий домена
|
||||
|
||||
## User
|
||||
|
||||
Локальный владелец данных WealthLedger, идентифицируемый `id` и
|
||||
нормализованным email. User владеет Session и T-Bank connection и видит только
|
||||
Brokerage Accounts, доступные через его текущее T-Bank connection.
|
||||
|
||||
_Не использовать_: клиент T-Bank, инвестор T-Bank.
|
||||
|
||||
## Session
|
||||
|
||||
Локальная authenticated-сессия User: короткоживущий access token в браузере и
|
||||
server-side refresh token rotation через cookie. Session не является
|
||||
T-Bank connection и не подтверждает доступ к Brokerage Account без текущего
|
||||
Bearer token.
|
||||
|
||||
_Не использовать_: T-Bank сессия, browser session.
|
||||
|
||||
## Money
|
||||
|
||||
Денежная сумма в одной ISO-валюте, передаваемая как `{ currency, value }`, где
|
||||
`value` — decimal string без экспоненциальной записи. Backend выполняет точные
|
||||
операции, frontend форматирует Money для отображения. Неизвестное или
|
||||
неприменимое денежное значение передаётся как `null`, а не как ноль и не как
|
||||
раздельные `amount`/`currency`.
|
||||
|
||||
_Не использовать_: сумма без валюты, транспортное представление как доменный термин.
|
||||
|
||||
## T-Bank connection
|
||||
|
||||
Единственное принадлежащее User подключение к T-Bank Invest, содержащее
|
||||
зашифрованный token для server-side запросов.
|
||||
|
||||
_Не использовать_: аккаунт T-Bank, авторизация T-Bank.
|
||||
|
||||
## Brokerage Account
|
||||
|
||||
Открытый поддерживаемый инвестиционный счёт T-Bank, доступный через текущее
|
||||
T-Bank connection User: брокерский счёт, ИИС, Инвесткопилка или фонд денежного
|
||||
рынка. WealthLedger не хранит Brokerage Account как долговременный объект
|
||||
владения независимо от T-Bank connection.
|
||||
|
||||
_Не использовать_: счёт T-Bank, банковский счёт.
|
||||
|
||||
## Investment Account Type
|
||||
|
||||
Нормализованная категория Brokerage Account, независимая от upstream enum
|
||||
T-Bank: `BROKERAGE`, `INDIVIDUAL_INVESTMENT`, `INVEST_BOX` или
|
||||
`MONEY_MARKET_FUND`.
|
||||
|
||||
_Не использовать_: тип аккаунта T-Bank, protobuf type.
|
||||
|
||||
## Portfolio
|
||||
|
||||
Текущее нормализованное состояние инвестиций одного Brokerage Account,
|
||||
включающее позиции, доступные денежные остатки и итоги по каждой валюте.
|
||||
|
||||
_Не использовать_: общий портфель, баланс счёта.
|
||||
|
||||
## Portfolio Position
|
||||
|
||||
Ненулевой обычный инвестиционный актив в составе Portfolio с количеством и
|
||||
доступными оценочными показателями. Portfolio Position не является Cash
|
||||
Balance, даже когда её type равен `CURRENCY`.
|
||||
|
||||
_Не использовать_: валютная позиция, виртуальная позиция.
|
||||
|
||||
## Portfolio Position Type
|
||||
|
||||
Нормализованный display type Portfolio Position, полученный из upstream
|
||||
instrument type: `STOCK`, `BOND`, `FUND`, `CURRENCY`, `FUTURE`, `OPTION`,
|
||||
`STRUCTURED_NOTE`, `DIGITAL_FINANCIAL_ASSET` или `OTHER`.
|
||||
|
||||
_Не использовать_: upstream instrument type как доменный enum.
|
||||
|
||||
## Currency Instrument Position
|
||||
|
||||
Portfolio Position с type `CURRENCY`, представляющая торгуемый валютный
|
||||
Instrument. Она остаётся в `positions` и не объединяется с Cash Balance той же
|
||||
валюты.
|
||||
|
||||
_Не использовать_: денежный остаток, cash balance.
|
||||
|
||||
## Instrument
|
||||
|
||||
Ценная бумага или иной торгуемый актив T-Bank, на который ссылается Portfolio
|
||||
Position или Future Cash Event через FIGI или instrument UID. Metadata
|
||||
Instrument обогащают отображение Position или Event, но не определяют
|
||||
количество, стоимость или факт события.
|
||||
|
||||
_Не использовать_: бумага, тикер, актив позиции.
|
||||
|
||||
## Instrument Logo
|
||||
|
||||
Имя файла бренда T-Bank для отображения Instrument в интерфейсе;
|
||||
presentation metadata, которая не влияет на количество или стоимость
|
||||
позиции. Изображение загружается frontend из публичного брендового CDN
|
||||
без авторизации; при отсутствии имени или ошибке загрузки используется
|
||||
фолбэк с первой буквой отображаемого имени.
|
||||
|
||||
_Не использовать_: аватар актива, иконка бумаги.
|
||||
|
||||
## Cash Balance
|
||||
|
||||
Ненулевой доступный денежный остаток Portfolio в одной валюте; не является
|
||||
Portfolio Position и передаётся отдельно от `positions` в `cashBalances`.
|
||||
|
||||
_Не использовать_: валютная позиция, заблокированные средства.
|
||||
|
||||
## Portfolio Snapshot
|
||||
|
||||
Полный успешно полученный состав Portfolio на определённый момент времени,
|
||||
который может быть свежим или устаревшим fallback-представлением.
|
||||
|
||||
_Не использовать_: история портфеля, snapshot для графика.
|
||||
|
||||
Следующие термины описывают runtime/cache-механизмы. Они не являются
|
||||
самостоятельными бизнес-сущностями.
|
||||
|
||||
## Client Cached Snapshot
|
||||
|
||||
Последний успешно полученный frontend-ответ React Query, который остаётся
|
||||
видимым во время фонового обновления или после его ошибки. Не является
|
||||
server-side fallback и не использует поле `Portfolio.isStale`.
|
||||
|
||||
_Не использовать_: устаревший Portfolio Snapshot, fallback портфеля.
|
||||
|
||||
## Account Ledger
|
||||
|
||||
Упорядоченная read-only история исполненных операций одного Brokerage Account,
|
||||
отражающая фактические денежные и инструментальные движения без рыночной
|
||||
переоценки или расчёта остатка после операции.
|
||||
|
||||
_Не использовать_: история Portfolio, баланс счёта.
|
||||
|
||||
## Ledger Entry
|
||||
|
||||
Нормализованное представление одной операции T-Bank в Account Ledger; хранит
|
||||
исходные суммы и доступные детали операции, не создавая синтетических записей
|
||||
для комиссии сделки. В публичном API история доступна через маршруты
|
||||
`/operations`; в доменной документации запись называется Ledger Entry.
|
||||
|
||||
_Не использовать_: проводка, рассчитанная операция.
|
||||
|
||||
## Operation Category
|
||||
|
||||
Нормализованная категория Ledger Entry для фильтрации и отображения:
|
||||
`trade`, `income`, `tax`, `fee`, `transfer` или `other`.
|
||||
|
||||
_Не использовать_: upstream operation type как category.
|
||||
|
||||
## Ledger Page Cache
|
||||
|
||||
Кэш страницы Account Ledger, отделённый от кэша отдельных операций.
|
||||
|
||||
_Не использовать_: operations cache, кэш отдельных операций.
|
||||
|
||||
## Ledger Page Cache Key
|
||||
|
||||
Каноническая идентичность страницы Ledger Page Cache: opaque generation
|
||||
текущего T-Bank connection, User, Brokerage Account, нормализованные диапазон
|
||||
и categories, opaque upstream cursor и limit.
|
||||
|
||||
_Не использовать_: сырой URL query, ключ отдельной Ledger Entry.
|
||||
|
||||
## Concurrent Refresh Attempt
|
||||
|
||||
Два или более перекрывающихся запроса с одним ещё действительным refresh token
|
||||
одной Session; один становится Refresh Winner, остальные — Refresh Loser и
|
||||
получают `401`, не отзывая Session победителя.
|
||||
|
||||
_Не использовать_: повторное использование refresh token, последовательный
|
||||
refresh.
|
||||
|
||||
## Superseded Refresh Token
|
||||
|
||||
Refresh token, который перестал быть действительным после успешной ротации
|
||||
Session; его предъявление получает `401`, но само по себе не отзывает Session.
|
||||
|
||||
_Не использовать_: активный refresh token, признак взлома сессии.
|
||||
|
||||
## Future Cash Event
|
||||
|
||||
Прогнозируемое на ближайший период денежное или связанное корпоративное
|
||||
событие одного Brokerage Account, рассчитанное по текущей Portfolio Position;
|
||||
не является исполненной Ledger Entry. Прогнозная сумма события передаётся как
|
||||
nullable Money: известные coupon, dividend и maturity содержат Money в исходной
|
||||
валюте, offer или неизвестные денежные условия передаются как `null`.
|
||||
В публичном API прогноз доступен через маршруты `/events`; в доменной
|
||||
документации событие называется Future Cash Event.
|
||||
|
||||
_Не использовать_: будущая операция, гарантированная выплата.
|
||||
|
||||
## Future Cash Event Type
|
||||
|
||||
Нормализованный тип Future Cash Event: `coupon`, `dividend`, `maturity` или
|
||||
`offer`.
|
||||
|
||||
_Не использовать_: upstream dividend type, MOEX row name.
|
||||
|
||||
## Realized Income Analytics
|
||||
|
||||
Read-only account-scoped result исполненных coupon и dividend Ledger Entries за
|
||||
включительный календарный period в Europe/Moscow. Он сохраняет исходные
|
||||
currencies и signed broker-reported payments; это не налоговый расчёт, не
|
||||
forecast и не investment performance.
|
||||
|
||||
_Не использовать_: доходность, общий доход в одной валюте, гарантированный
|
||||
доход.
|
||||
@@ -32,10 +32,13 @@ message/stack. Типичный ответ:
|
||||
не считается проверенным одним описанием OpenAPI: нужна проверка реального
|
||||
HTTP-ответа после всех стадий обработки запроса.
|
||||
|
||||
**Требует ручной проверки:** согласованность деталей ошибок валидации полей
|
||||
в полном HTTP-потоке пока не подтверждена. Известно расхождение формата ошибок
|
||||
между стадиями валидации и формирования ответа; наличие описания в OpenAPI
|
||||
не доказывает передачу ожидаемых `pointer` и `detail`.
|
||||
**Известное расхождение:** согласованная
|
||||
[спецификация аутентификации](../../specs/openspec/specs/user-authentication/spec.md#requirement-стандартные-ответы-об-ошибках)
|
||||
требует `pointer` и `detail` для невалидных полей. В текущей конфигурации
|
||||
валидация формирует строковые сообщения, а фильтр ожидает структурированные
|
||||
ошибки полей; поэтому корректность деталей `errors` не подтверждена.
|
||||
Исправление требует отдельного изменения реализации и проверки полного
|
||||
HTTP-потока. OpenAPI сам по себе этого расхождения не устраняет.
|
||||
|
||||
Ответ не включает автоматически все поля RFC (`type`, `title`, `instance`).
|
||||
Добавление обязательного поля является изменением публичного контракта.
|
||||
@@ -45,9 +48,9 @@ HTTP-ответа после всех стадий обработки запро
|
||||
|
||||
Специальный code определяется сценарием; общее преобразование ошибок и безопасная
|
||||
форма ответа принадлежат HTTP-границе. Auth endpoints используют свои status и cookie
|
||||
operations: [Authentication](../features/authentication.md).
|
||||
operations: [Authentication](../domains/authentication/overview.md).
|
||||
T-Bank token failure и временная недоступность имеют разные значения:
|
||||
[Connection API](../features/tbank-connection.md#api). Raw upstream/Prisma errors
|
||||
[Connection API](../integrations/tbank.md#состояние-и-ошибки). Raw upstream/Prisma errors
|
||||
и secrets не передаются frontend.
|
||||
|
||||
## Граница доказательства
|
||||
@@ -57,4 +60,25 @@ T-Bank token failure и временная недоступность имеют
|
||||
Валидация, доступ, статусы и cookie требуют проверки полного HTTP-потока;
|
||||
атомарность хранения — проверки с изолированной реальной базой.
|
||||
Известное ограничение сквозных проверок описано в
|
||||
[проверке backend](../development/backend-workflow.md#локальная-проверка).
|
||||
[проверке backend](../development/verification.md).
|
||||
|
||||
## Наблюдаемость
|
||||
|
||||
Backend пишет структурированные JSON-события в stdout через Pino. Обработка
|
||||
HTTP-запроса получает серверный request ID, который возвращается заголовком
|
||||
`X-Request-ID`; допустимый входящий идентификатор сохраняется отдельно как
|
||||
client request ID. Это позволяет сопоставить публичную ошибку с обработкой
|
||||
запроса без передачи внутреннего exception в ответ API.
|
||||
|
||||
Журнал завершения запроса содержит шаблон маршрута, метод, статус,
|
||||
длительность и при наличии публичный error code. События внешней границы
|
||||
добавляют операцию, результат и число попыток. Разрешённые поля ограничены;
|
||||
полные запросы и ответы, token, cookie и Authorization не являются данными
|
||||
журнала. Фильтрация полей и redaction не защищают секрет, вручную включённый
|
||||
в свободный текст сообщения или stack.
|
||||
|
||||
Выбор logger и граница ответственности описаны в
|
||||
[ADR 008](../adr/008-pino-backend-logging.md); согласованное поведение — в
|
||||
workspace-спецификации
|
||||
[Structured logging](../../specs/openspec/specs/backend-structured-logging/spec.md).
|
||||
Сборщик, срок хранения журналов и alerting не определяются приложением.
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
# Обзор системы
|
||||
|
||||
WealthLedger — локальное приложение для частного инвестора: оно связывает
|
||||
состав портфеля, операции, полученный доход и будущие выплаты по счетам T-Bank.
|
||||
Обзор объясняет систему сверху вниз. Детали сценариев и ответственность подсистем находятся
|
||||
в [карте возможностей](../features/README.md).
|
||||
|
||||
## Поставленные возможности
|
||||
|
||||
- [Аутентификация](../features/authentication.md): access/refresh-сессии и
|
||||
защищённые маршруты.
|
||||
- [Подключение T-Bank](../features/tbank-connection.md): один зашифрованный
|
||||
token на User.
|
||||
- [Брокерские счета](../features/brokerage-accounts.md),
|
||||
[Portfolio](../features/portfolio-view.md),
|
||||
[Account Ledger](../features/brokerage-account-ledger.md),
|
||||
[Пассивный доход](../features/brokerage-account-income-analytics.md) и
|
||||
[Future Cash Events](../features/brokerage-account-upcoming-events.md).
|
||||
- [Frontend UI resilience](../features/frontend-ui-resilience.md): явные
|
||||
loading, error, empty и cached-snapshot состояния; initial и частичные
|
||||
loading-состояния показывают structural skeleton, а уже доступный контекст
|
||||
остаётся видимым при фоновом обновлении или действии пользователя.
|
||||
- [Все счета](../features/all-accounts.md): общий портфель, журнал, доходы
|
||||
и будущие выплаты с сохранением принадлежности к счетам.
|
||||
- [Акции](../features/stock-detail.md) и [облигации](../features/bond-detail.md):
|
||||
поиск, рыночные данные, личная позиция и события конкретного инструмента.
|
||||
|
||||
Термины этих возможностей определяет [глоссарий домена](domain-glossary.md), а
|
||||
причины ключевых технических решений — [ADR](../adr/README.md).
|
||||
|
||||
## Контекст системы
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
U[Частный инвестор] -->|Смотрит инвестиции| W[WealthLedger]
|
||||
W -->|Счета, позиции, операции и рынок| T[T-Bank Invest API]
|
||||
W -->|Публичные данные облигаций| M[MOEX ISS]
|
||||
W -->|Рейтинги выпусков| N[Реестр НКР]
|
||||
W -->|Изображения логотипов| C[T-Bank Brands CDN]
|
||||
```
|
||||
|
||||
T-Bank — источник личных брокерских данных и рынка. MOEX дополняет будущие
|
||||
события и показатели облигаций, НКР — рейтинг конкретного выпуска. Публичный
|
||||
рейтинг или календарь выпуска не подтверждает личное зачисление инвестору.
|
||||
Отсутствие записи и недоступность источника имеют разный смысл; детали —
|
||||
[Bond detail](../features/bond-detail.md).
|
||||
|
||||
## Исполняемые части
|
||||
|
||||
На уровне C4 containers здесь два приложения и локальная база. Каталоги
|
||||
`docs` и `specs` не являются runtime-компонентами.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph Browser[Браузер пользователя]
|
||||
F[React SPA]
|
||||
end
|
||||
subgraph Local[Локальное окружение]
|
||||
B[NestJS API]
|
||||
D[(SQLite)]
|
||||
B -->|Prisma: identity, sessions, connection| D
|
||||
end
|
||||
F -->|HTTP JSON, Bearer и auth cookie| B
|
||||
B -->|gRPC over TLS, token пользователя| T[T-Bank Invest]
|
||||
B -->|HTTPS, публичные данные| M[MOEX ISS]
|
||||
B -->|HTTPS, публичный HTML| N[НКР]
|
||||
F -->|HTTPS, изображения| C[Brands CDN]
|
||||
```
|
||||
|
||||
- **Frontend** владеет представлением, навигацией и клиентским query cache.
|
||||
Access token существует в памяти; refresh-cookie обслуживает browser.
|
||||
- **Backend** владеет identity, проверкой доступа, credentials брокера,
|
||||
нормализацией, денежными расчётами и upstream-кэшем.
|
||||
- **SQLite** хранит локального User, sessions и зашифрованное подключение.
|
||||
- **Интеграции backend** обращаются к T-Bank, MOEX и НКР, сохраняя различия
|
||||
личных данных брокера и публичных данных выпуска. Браузер получает
|
||||
изображения логотипов непосредственно из публичного CDN.
|
||||
|
||||
Frontend связывает пользовательские экраны навигацией. Backend объединяет
|
||||
аутентификацию, подключение брокера и инвестиционные сценарии через публичный API.
|
||||
Согласование адресов и origin — [локальный запуск](../development/local-workspace.md).
|
||||
|
||||
## Хранимые данные
|
||||
|
||||
Локальная база содержит идентичность пользователя и hash пароля, состояние
|
||||
его сессий со сроком действия, признаком отзыва и hash текущего refresh token,
|
||||
а также одно зашифрованное подключение T-Bank с временем подключения.
|
||||
Сессии и подключение принадлежат конкретному пользователю; plaintext пароля
|
||||
или broker token не является сохраняемым представлением credentials.
|
||||
|
||||
Изменения связанных локальных данных требуют сохранения их целостности.
|
||||
Ротация сессии атомарно заменяет текущий refresh token: конкурентные запросы
|
||||
не могут оба выиграть. Сетевой вызов брокера и изменение локальной базы не
|
||||
образуют одну общую транзакцию. Финансовые наблюдения хранятся в ограниченном
|
||||
кэше, а не как долговременная история брокерских данных.
|
||||
|
||||
## Локальные границы
|
||||
|
||||
- Финансовые API-запросы frontend проходят backend; browser не получает
|
||||
T-Bank credential или gRPC client. Загрузка публичных логотипов — отдельный поток.
|
||||
- Backend — NestJS API `/api/v1`, владеющий auth, SQLite-данными и T-Bank
|
||||
gRPC boundary.
|
||||
- OpenAPI backend — единственный контракт frontend; committed client
|
||||
генерируется штатным workflow.
|
||||
- Поставка остаётся локальной: deployment, Docker, CI/CD и облачные сервисы не
|
||||
входят в текущие границы.
|
||||
- Portfolio Snapshot, summary списка Brokerage Account, Future Cash Events и
|
||||
Ledger pages существуют как read-only наблюдения текущего T-Bank connection;
|
||||
система не хранит историю портфеля, будущих событий или брокерских счетов как
|
||||
долговременные агрегаты. Summary списка сохраняет исходные валюты без FX-
|
||||
агрегации и может быть локально недоступна для отдельного счета.
|
||||
|
||||
## Предметная модель
|
||||
|
||||
Локальный [User](domain-glossary.md#user) владеет
|
||||
[Session](domain-glossary.md#session) и одним
|
||||
[T-Bank connection](domain-glossary.md#t-bank-connection). Через подключение
|
||||
backend устанавливает доступ к [Brokerage Accounts](domain-glossary.md#brokerage-account).
|
||||
Идентификатор счёта из URL сам по себе не подтверждает этот доступ.
|
||||
|
||||
[Portfolio](domain-glossary.md#portfolio) отвечает на вопрос «что есть сейчас»,
|
||||
[Account Ledger](domain-glossary.md#account-ledger) — «что исполнено»,
|
||||
[Realized Income](domain-glossary.md#realized-income-analytics) — «какие выплаты
|
||||
уже получены», будущие события — «что ожидается». Эти представления нельзя
|
||||
подменять друг другом. [Money](domain-glossary.md#money) сохраняет исходную
|
||||
валюту и точность; неизвестное значение отличается от нуля.
|
||||
|
||||
Режим [«Все счета»](../features/all-accounts.md) агрегирует доступный состав,
|
||||
сохраняя происхождение строк; он не создаёт новый брокерский счёт. Обновление
|
||||
подключения меняет границу финансовых кэшей; перезапуск backend не восстанавливает
|
||||
прежние in-memory snapshots и cursor continuations из SQLite.
|
||||
|
||||
## Поток auth/session
|
||||
|
||||
Вход создаёт локальную сессию; последующие запросы используют access token.
|
||||
После перезагрузки SPA восстанавливает сессию через refresh-cookie. Схема,
|
||||
конкурентная ротация, logout и ограничения собраны у одного owner:
|
||||
[Authentication](../features/authentication.md).
|
||||
|
||||
## Поток инвестиционных данных
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as User
|
||||
participant F as Frontend
|
||||
participant B as Backend
|
||||
participant D as SQLite
|
||||
participant T as T-Bank gRPC
|
||||
U->>F: Открывает счета или Portfolio
|
||||
F->>B: GET /api/v1/brokerage-accounts/* (Bearer)
|
||||
B->>D: Читает подключение текущего User
|
||||
B->>T: Проверяет доступ к счёту
|
||||
B->>T: Запрос через server-side gRPC boundary
|
||||
T-->>B: Upstream-данные
|
||||
B-->>F: Нормализованные данные или Problem Details
|
||||
F-->>U: Экран с данными или локальным состоянием ошибки
|
||||
```
|
||||
|
||||
Схема показывает запрос, которому нужны upstream-данные. Повторные чтения могут
|
||||
обслуживаться кэшем по правилам конкретной feature. Проверку connection/account
|
||||
выполняет backend: запрос связывается с пользователем, его текущим подключением
|
||||
и доступными через него счетами, включая выдачу данных из кэша.
|
||||
Внешняя ошибка не становится нулевым балансом; допустимость stale snapshot и
|
||||
локальное восстановление описывает соответствующий сценарий.
|
||||
|
||||
Причины server-side границы, шифрования и retry policy —
|
||||
[ADR 003](../adr/003-tbank-server-side-grpc-boundary.md),
|
||||
[ADR 004](../adr/004-encrypted-per-user-tbank-token.md) и
|
||||
[ADR 005](../adr/005-tbank-upstream-resilience.md).
|
||||
Reference in New Issue
Block a user