spec: finalize wealth-ledger SDD migration
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 11s

This commit was merged in pull request #26.
This commit is contained in:
2026-10-09 21:14:22 +03:00
committed by ksv741
parent 90edab6e33
commit 329d66e767
97 changed files with 3444 additions and 244 deletions
@@ -0,0 +1,144 @@
## MODIFIED Requirements
### Requirement: Получение Portfolio выбранного Brokerage Account
Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts/:accountId/portfolio`. Она MUST возвращать нормализованный полный Portfolio Snapshot только для открытого поддерживаемого Brokerage Account, доступного через текущий T-Bank connection User. Ответ MUST иметь envelope `{ data, meta }` по контракту `tbank-data-sync`; сохранённый полный Snapshot в `data` MUST содержать `lastSuccessfulAt` в RFC 3339 UTC без миллисекунд, boolean `isStale`, `totalsByCurrency`, `positions` и `cashBalances`.
Portfolio Snapshot является результатом наблюдения Portfolio на конкретный момент. Система MUST сохранять последний полный Snapshot постоянно, без накопления исторических снимков стоимости. Это MUST NOT подтверждать доступность Brokerage Account вне текущего T-Bank connection. Проверка локальной сессии, текущего подключения и сохранённого состава доступных счетов MUST выполняться без синхронного запроса списка счетов T-Bank. При отсутствии сохранённого Snapshot ответ MUST иметь `200`, `data: null`, `meta.availability: missing` и состояние запрошенной загрузки; это MUST NOT обозначать пустой Portfolio.
#### Scenario: Получение текущего Portfolio
- **WHEN** User запрашивает Portfolio доступного Brokerage Account, а полный Snapshot текущего подключения успешно сохранён и ещё свеж
- **THEN** система отвечает `200` с полным нормализованным Portfolio Snapshot в `data` без ожидания T-Bank с `isStale: false` и временем успешного получения финансового снимка
#### Scenario: Недоступный Brokerage Account
- **WHEN** для текущего подключения подтверждено, что `accountId` не принадлежит User, закрыт, неподдерживаем или больше не доступен
- **THEN** система отвечает `404` без Portfolio Snapshot
#### Scenario: Сохранённый Portfolio читается после перезапуска
- **WHEN** backend перезапущен, текущий connection не изменился и полный Snapshot уже сохранён
- **THEN** запрос возвращает его из постоянного хранилища без ожидания T-Bank и отдельно сообщает свежесть и состояние фонового обновления
### Requirement: Свежесть и fallback полного Portfolio Snapshot
Система MUST постоянно сохранять последний полный успешный Portfolio Snapshot отдельно для User, Brokerage Account и текущего T-Bank connection. Обычный запрос MUST возвращать сохранённый Snapshot без ожидания T-Bank; при возрасте не менее одной минуты он MUST запросить фоновое обновление. Временная ошибка MUST NOT удалять Snapshot или ограничивать его выдачу прежним пределом пяти минут. `lastSuccessfulAt` MUST сохранять время успешного получения финансового снимка и совпадать с соответствующим временем в `meta`; `isStale` MUST отражать `meta.freshness: stale`, а не состояние клиентского кэша. Ручное действие «Обновить» MUST обходить проверку давности текущего Portfolio и его показанных зависимостей. Неуспешное ручное обновление MUST сохранять исходное `lastSuccessfulAt` и MUST NOT само по себе устанавливать `isStale: true`, пока минутный срок свежести сохранённого финансового снимка не истёк; ошибка MUST отображаться отдельно через `meta.sync`.
Metadata инструмента MUST NOT влиять на успешность или свежесть Portfolio Snapshot. Сохранённые metadata MUST использоваться без ожидания их обновления, а отсутствующие сведения MUST допускать существующий fallback идентификатора. Новые доступные metadata MUST дополнять представление, не меняя времени финансового снимка. Общие данные инструментов MUST следовать суточной свежести `tbank-data-sync`, без сохранения персональных позиций в общих данных.
После успешной замены или удаления T-Bank connection прежние Snapshot и поздние результаты его запросов MUST NOT обслуживать новое подключение. Известный отказ token текущего подключения MUST блокировать личное чтение с существующим `INVALID_TBANK_TOKEN`; он MUST NOT маскироваться сохранённым Snapshot.
#### Scenario: Свежий Snapshot
- **WHEN** User повторно запрашивает тот же Portfolio ранее чем через одну минуту после успешного получения
- **THEN** система возвращает свежий Snapshot без нового обращения к T-Bank
#### Scenario: Медленный lookup инструмента
- **WHEN** metadata инструмента ещё отсутствует и его lookup не завершился
- **THEN** система возвращает сохранённый финансовый Snapshot без ожидания lookup с доступным fallback, а после успешного lookup последующее чтение использует полученное name
#### Scenario: Fallback после временной upstream-ошибки
- **WHEN** обновление устаревшего Snapshot неуспешно из-за временной недоступности T-Bank, а последний полный успешный Snapshot получен более пяти минут назад
- **THEN** система отвечает `200` этим Snapshot с `isStale: true`, исходным `lastSuccessfulAt` и ошибкой обновления в `meta.sync`, без повторной синхронной попытки
#### Scenario: Ручное обновление ещё свежего Snapshot неуспешно
- **WHEN** User принудительно обновляет Snapshot, полученный менее минуты назад, и попытка завершается временной ошибкой до истечения его TTL
- **THEN** наблюдение возвращает прежний Snapshot с `isStale: false`, исходным `lastSuccessfulAt` и отдельной ошибкой синхронизации; ошибка не продлевает TTL и не делает данные устаревшими раньше срока
#### Scenario: Недействительный token не маскируется fallback-данными
- **WHEN** фоновое обновление подтвердило отклонение token текущего подключения как неаутентифицированного или без прав, после чего запрошен Portfolio
- **THEN** система не возвращает fallback Snapshot и отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN`
### Requirement: Состояния и ошибки Portfolio на клиенте
Клиент SHALL загружать Portfolio при открытии защищённого маршрута
`/accounts/:accountId`. Он MUST использовать успешный ответ в течение
30 секунд, без automatic retry неуспешного запроса, периодической синхронизации или refetch при возврате фокуса. Наблюдение за состоянием уже запрошенной работы MUST следовать `tbank-data-sync` и MUST NOT запускать новую работу.
При загрузке без ранее полученного успешного ответа либо при `data: null`
с состоянием загрузки для этого же Brokerage Account клиент MUST показывать структурное skeleton-состояние ожидаемого
содержимого, включая сводку, без вымышленных сумм. Область загрузки MUST
иметь доступное обозначение занятости; skeleton MUST NOT быть интерактивным
или получать клавиатурный фокус.
Истечение срока свежести MUST NOT само по себе скрывать уже доступный
Client Cached Snapshot. Во время фонового обновления и после его временной ошибки
клиент MUST сохранять ранее полученные данные того же Portfolio, включая
валютные итоги и исходное `lastSuccessfulAt`. При обновлении клиент MUST
показывать компактную неблокирующую индикацию без замены данных skeleton.
При ошибке обновления он MUST показывать локализованное неблокирующее
предупреждение о последних полученных данных и предоставлять действие
повторной попытки. Сохранённые данные MUST NOT считаться новым успешным
снимком или подтверждением текущей доступности Brokerage Account.
Клиент MUST показать штатное empty-состояние только при `meta.availability: complete`, непустом `data` и пустых `positions`
и `cashBalances`, без нулевых валютных карточек. Для показываемого состава
Portfolio клиент MUST сохранять время успешного получения снимка
`lastSuccessfulAt`, форматируемое в UTC. При `isStale: true` клиент MUST
показывать Snapshot, его исходное время и нейтральное предупреждение
об устаревших данных. Ошибка синхронизации MUST иметь отдельное безопасное объяснение. На экране MUST быть доступно действие «Обновить», обновляющее текущий экран и его зависимости независимо от давности. Server-side stale и ошибка фонового
обновления Client Cached Snapshot MUST оставаться раздельными состояниями:
ошибка клиента MUST NOT менять или подменять `isStale`, а оба предупреждения
и действие повтора клиентского обновления MUST оставаться доступными при
одновременном наличии этих состояний.
При ответе `404` для этого же Portfolio клиент MUST показать состояние недоступного счёта
с действием возврата к списку счетов без действия повтора. При `TBANK_NOT_CONNECTED` или `INVALID_TBANK_TOKEN` клиент MUST скрыть личные данные даже при наличии Snapshot. При отсутствии Snapshot и `TBANK_UNAVAILABLE`
или иной ошибке клиент MUST показать безопасное error-состояние с доступными
для ошибки действиями; действие повтора разрешено только для временной
и неизвестной ошибки.
#### Scenario: Показ устаревшего Portfolio
- **WHEN** клиент получает успешный ответ с `isStale: true`
- **THEN** клиент показывает позиции, остатки, валютные итоги, время
последнего успешного снимка, нейтральное предупреждение об устаревании
и доступное действие «Обновить»
#### Scenario: Недоступный счёт на клиенте
- **WHEN** клиент получает `404` для текущего Portfolio
- **THEN** клиент не показывает Portfolio и предлагает вернуться к списку
Brokerage Account без действия повтора
#### Scenario: Первичная загрузка Portfolio
- **WHEN** клиент ожидает Portfolio этого Brokerage Account без ранее
полученного успешного ответа либо получает `data: null` с активной загрузкой
- **THEN** он показывает структурный skeleton, включая место сводки,
с доступным обозначением загрузки, без вымышленных или нулевых сумм
и без фокусируемых placeholders
#### Scenario: Обновление после истечения срока свежести
- **WHEN** срок свежести показанного Portfolio истёк и началось фоновое
обновление данных того же Brokerage Account
- **THEN** клиент сохраняет прежние валютные итоги и время их успешного
получения, показывает неблокирующую индикацию обновления и не заменяет
доступный состав и сводку skeleton-состоянием
#### Scenario: Ошибка обновления после успешного ответа
- **WHEN** клиент уже показывает Client Cached Snapshot Portfolio,
а обновление этих же данных завершается временной ошибкой
- **THEN** клиент сохраняет данные, суммы и время прежнего снимка,
показывает неблокирующее предупреждение и действие повтора;
ошибка не делает сохранённый снимок свежим и не изменяет `isStale`
#### Scenario: Ошибка клиентского обновления при server-side stale
- **WHEN** клиент показывает Portfolio с `isStale: true`, а фоновое
обновление этого же Portfolio завершается временной ошибкой
- **THEN** прежние суммы и время остаются видимыми одновременно
с нейтральным server-side предупреждением, отдельным предупреждением
об ошибке клиентского обновления и его действием повтора
#### Scenario: Повторное обновление успешно
- **WHEN** после ошибки фонового обновления User запускает повторную
попытку и получает успешный Portfolio Snapshot
- **THEN** до ответа прежние суммы и время остаются видимыми, затем
клиент показывает значения и `lastSuccessfulAt` успешного ответа,
убирает предупреждение об ошибке клиентского обновления и показывает
server-side предупреждение только в соответствии с новым `isStale`
#### Scenario: Штатный пустой Portfolio на клиенте
- **WHEN** успешно полученный ответ имеет `meta.availability: complete` и Portfolio в `data` содержит пустые `positions`
и `cashBalances`
- **THEN** клиент показывает существующее empty-состояние без валютных
карточек с вымышленными нулевыми итогами