diff --git a/architecture/domain-glossary.md b/architecture/domain-glossary.md index d14a94e..9ca7544 100644 --- a/architecture/domain-glossary.md +++ b/architecture/domain-glossary.md @@ -159,8 +159,9 @@ _Не использовать_: operations cache, кэш отдельных о ## Ledger Page Cache Key -Каноническая идентичность страницы Ledger Page Cache: User, Brokerage Account, -нормализованные диапазон и categories, opaque upstream cursor и limit. +Каноническая идентичность страницы Ledger Page Cache: opaque generation +текущего T-Bank connection, User, Brokerage Account, нормализованные диапазон +и categories, opaque upstream cursor и limit. _Не использовать_: сырой URL query, ключ отдельной Ledger Entry. diff --git a/features/brokerage-account-ledger.md b/features/brokerage-account-ledger.md index 317878d..68b265d 100644 --- a/features/brokerage-account-ledger.md +++ b/features/brokerage-account-ledger.md @@ -37,6 +37,6 @@ filters и данные сохраняют существующую семант - Полный журнал фильтруется по периоду (два DateInput «С»/«По» с форматом DD.MM.YYYY и min/max-ограничением, русская локаль календаря) и категориям (MultiSelect с мультивыбором из trade, income, tax, fee, transfer, other). Фильтры имеют черновик в контролах и применённое состояние: изменение черновика не вызывает новый запрос к серверу; «Применить» коммитит черновик и сбрасывает cursor-пагинацию на первую страницу; «Сбросить» очищает черновик и немедленно применяет пустой фильтр (период по умолчанию и все категории). Cursor-пагинация («Назад»/«Далее») работает по последнему применённому фильтру, не по черновику. - В полном журнале и пяти последних записях обзора одна и та же компактная строка показывает логотип инструмента либо буквенный fallback, описание операции, дату, инструмент и денежное движение. Денежная метка строки показывает подпись конкретного типа события: для income-записи — «Купон» или «Дивиденд» (по upstream-типу купона или дивиденда), для остальных — подпись категории («Сделка», «Налог», «Комиссия», «Перевод», «Прочее»). Подпись типа выводится один раз и не повторяется во вторичной метадате (дата · инструмент); направление передаётся знаком «+» / «−` и семантическим цветом, а не текстом. Записи без движения денег остаются нейтральными («Без движения денег»). - Полный журнал не показывает цену, количество, НКД и комиссию записи. -- Backend хранит в памяти процесса до 100 страниц Account Ledger: свежая страница живёт до 30 секунд, а при переполнении вытесняется давно не использовавшаяся страница (LRU). Ключ включает пользователя, счёт, нормализованные период и categories, cursor и limit; отсутствующий `to` приводится к текущему 30-секундному bucket. Проверка подключения и доступа к счёту выполняется до выдачи кэшированной страницы. Ключ не включает версию T-Bank token, поэтому после успешной замены T-Bank connection для того же User, Brokerage Account и query уже сохранённая страница может оставаться видимой до истечения 30-секундного TTL. Операции не сохраняются и не синхронизируются локально. +- Backend хранит в памяти процесса до 100 страниц Account Ledger: свежая страница живёт до 30 секунд, а при переполнении вытесняется давно не использовавшаяся страница (LRU). Ключ включает непрозрачную generation текущего T-Bank connection, пользователя, счёт, нормализованные период и categories, cursor и limit; отсутствующий `to` приводится к текущему 30-секундному bucket. Проверка connection и доступа к счёту выполняется до выдачи кэшированной страницы. После успешного создания, замены или удаления connection сохранённая либо поздно завершившаяся страница предыдущей generation недоступна Operations, Income Summary и Income Analytics. Старые страницы удаляются обычными TTL/LRU limits; операции не сохраняются и не синхронизируются локально. - Журнал не рассчитывает остаток после операции, суммарные доходы или налоги, историческую стоимость Portfolio, конвертацию валют и рыночные изменения цены. - `409 TBANK_NOT_CONNECTED`, `400 INVALID_TBANK_TOKEN`, `404` недоступного счёта и `503 TBANK_UNAVAILABLE` используют существующую семантику ошибок T-Bank. diff --git a/features/portfolio-view.md b/features/portfolio-view.md index a294036..ed2c443 100644 --- a/features/portfolio-view.md +++ b/features/portfolio-view.md @@ -52,13 +52,14 @@ section; Portfolio и соседние sections остаются доступн декоративны, не содержат пользовательских данных и не получают keyboard focus. - Backend хранит успешный полный Snapshot для пары User и Brokerage Account свежим 30 секунд. -- После истечения fresh TTL последний Snapshot можно вернуть как fallback не более пяти минут только при `TBANK_UNAVAILABLE`; такой ответ имеет `isStale: true` и исходный `lastSuccessfulAt`. +- До cache hit backend разрешает текущее persisted T-Bank connection. Snapshot повторно используется только внутри непрозрачной generation этого connection; при cache miss доступ к Brokerage Account проверяется до финансовых T-Bank RPC. +- После истечения fresh TTL последний Snapshot той же generation можно вернуть как fallback не более пяти минут только при `TBANK_UNAVAILABLE`; такой ответ имеет `isStale: true` и исходный `lastSuccessfulAt`. - `INVALID_TBANK_TOKEN` никогда не маскируется fallback-данными. -- Успешная замена или удаление T-Bank connection очищает Portfolio Snapshot User. +- Успешное создание, замена или удаление T-Bank connection меняет cache boundary. Сохранённый либо поздно завершившийся Snapshot предыдущей generation не выдаётся новому запросу; явная очистка при mutation остаётся best-effort освобождением памяти. - Metadata по FIGI и подтверждённое отсутствие инструмента кэшируются в памяти 24 часа глобально; ошибки lookup не кэшируются. - `409 TBANK_NOT_CONNECTED` и `400 INVALID_TBANK_TOKEN` ведут в настройки; `404` показывает возврат к списку счетов; `503 TBANK_UNAVAILABLE` и неизвестная ошибка позволяют повторить запрос, если нет fallback Snapshot. -Клиент использует успешный React Query-кэш 30 секунд, без polling, automatic retry и refetch при возврате фокуса. При фоновом обновлении уже полученный Portfolio остаётся видимым как **Client Cached Snapshot**; отдельные индикатор обновления и предупреждение с повторной попыткой показывают его состояние. Это не server-side fallback: `Portfolio.isStale` сообщает только о fallback Snapshot от backend и может отображаться одновременно с client refresh warning. Акции и облигации отображаются отдельными секциями с upstream-порядком внутри каждой; остальные активы остаются в отдельной секции. Карточки акций и облигаций выводятся одноколоночным grid при любой ширине экрана и показывают Instrument Logo слева от текстового блока (аватар 40px с фолбэком на первую букву отображаемого имени при отсутствии `logoName` или ошибке загрузки), идентификатор, количество, текущую стоимость и абсолютный доход со знаком `+`/`-` без текстовой подписи (ноль -- без знака) с цветовой индикацией знака (зелёный при положительном, красный при отрицательном). Карточка акции группирует логотип, имя, количество и ticker слева; при `ticker: null` вместо него показывает ISIN. Карточка облигации и плитка актива иного типа сохраняют ISIN. Стоимость с доходностью остаются справа, а средняя цена и доля показываются одной приглушённой строкой под разделителем. Карточки остальных активов остаются компактными плитками в адаптивном grid -- одна колонка на узком экране, 2-3 на широком, -- с текстовой подписью абсолютного дохода и без визуального логотипа даже при наличии `logoName`. Плитка показывает идентификатор, количество, текущую стоимость, абсолютный доход с цветовой индикацией знака (зелёный при положительном, красный при отрицательном), среднюю цену покупки, долю среди позиций той же валюты и ISIN. Доля -- display-only оценка, вычисляемая на клиенте как `currentValue` позиции к сумме `currentValue` позиций той же валюты, без Cash Balance; отсутствующие метрики скрываются, а не отображаются заглушкой. RUB форматируется как `₽`. Loaded view сохраняет адаптивное одноколоночное отображение на узком экране. +Клиент использует успешный React Query-кэш 30 секунд, без polling, automatic retry и refetch при возврате фокуса. Successful connection mutation сбрасывает Portfolio query вместе со всем brokerage subtree, поэтому предыдущий Snapshot не показывается во время загрузки новой generation. При обычном фоновом обновлении без смены connection уже полученный Portfolio остаётся видимым как **Client Cached Snapshot**; отдельные индикатор обновления и предупреждение с повторной попыткой показывают его состояние. Это не server-side fallback: `Portfolio.isStale` сообщает только о fallback Snapshot от backend и может отображаться одновременно с client refresh warning. Акции и облигации отображаются отдельными секциями с upstream-порядком внутри каждой; остальные активы остаются в отдельной секции. Карточки акций и облигаций выводятся одноколоночным grid при любой ширине экрана и показывают Instrument Logo слева от текстового блока (аватар 40px с фолбэком на первую букву отображаемого имени при отсутствии `logoName` или ошибке загрузки), идентификатор, количество, текущую стоимость и абсолютный доход со знаком `+`/`-` без текстовой подписи (ноль -- без знака) с цветовой индикацией знака (зелёный при положительном, красный при отрицательном). Карточка акции группирует логотип, имя, количество и ticker слева; при `ticker: null` вместо него показывает ISIN. Карточка облигации и плитка актива иного типа сохраняют ISIN. Стоимость с доходностью остаются справа, а средняя цена и доля показываются одной приглушённой строкой под разделителем. Карточки остальных активов остаются компактными плитками в адаптивном grid -- одна колонка на узком экране, 2-3 на широком, -- с текстовой подписью абсолютного дохода и без визуального логотипа даже при наличии `logoName`. Плитка показывает идентификатор, количество, текущую стоимость, абсолютный доход с цветовой индикацией знака (зелёный при положительном, красный при отрицательном), среднюю цену покупки, долю среди позиций той же валюты и ISIN. Доля -- display-only оценка, вычисляемая на клиенте как `currentValue` позиции к сумме `currentValue` позиций той же валюты, без Cash Balance; отсутствующие метрики скрываются, а не отображаются заглушкой. RUB форматируется как `₽`. Loaded view сохраняет адаптивное одноколоночное отображение на узком экране. ## Ограничения MVP diff --git a/features/tbank-connection.md b/features/tbank-connection.md index bdc2b5c..c7b1baf 100644 --- a/features/tbank-connection.md +++ b/features/tbank-connection.md @@ -24,6 +24,26 @@ sequenceDiagram `INVALID_TBANK_TOKEN` означает, что T-Bank отклонил аутентификацию или права доступа. `TBANK_UNAVAILABLE` означает временную ошибку, превышение лимита, ожидание в очереди или другую ошибку внешнего сервиса. +## Граница финансового кэша + +Каждый успешный `PUT` заново шифрует token с новым AES-GCM nonce. Backend +вычисляет из сохранённого шифротекста непрозрачную внутреннюю generation и +разрешает повторно использовать Portfolio Snapshot и Ledger Page Cache только +для текущей generation. Generation не хранится отдельным полем, не входит в +API, logs или errors. Поэтому сохранённый или поздно завершившийся ответ +предыдущего подключения не обслуживает запрос после создания, замены или +удаления connection, включая замену тем же plaintext token. + +После успешного `PUT` или `DELETE` frontend сначала коммитит подтверждённый +статус connection, затем сбрасывает все generated GET query в +`/api/v1/brokerage-accounts` subtree. Активные выборки запрашиваются заново, а +неактивные загружаются при следующем использовании; данные предыдущей границы +не остаются Client Cached Snapshot. Неуспешная mutation не меняет status, +backend generation или financial cache. Обычная ошибка фонового refetch +сохраняет Client Cached Snapshot текущей generation. Logout остаётся отдельной +границей Session и полностью очищает status connection и остальные +session-scoped query. + ## Конфигурация и ограничения - `TOKEN_ENCRYPTION_KEY` - закодированный в base64 32-байтный ключ AES-256-GCM; обязателен при запуске.