## 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-состояние без валютных карточек с вымышленными нулевыми итогами