diff --git a/openspec/changes/archive/2026-08-26-enhance-brokerage-account-list/.openspec.yaml b/openspec/changes/archive/2026-08-26-enhance-brokerage-account-list/.openspec.yaml new file mode 100644 index 0000000..0772cee --- /dev/null +++ b/openspec/changes/archive/2026-08-26-enhance-brokerage-account-list/.openspec.yaml @@ -0,0 +1,9 @@ +schema: wealth-ledger-sdd +created: 2026-08-26 +goal: Improve the all-accounts overview with comparable current portfolio summaries + and an informative, accessible account-list interface. +affected_areas: + - frontend + - backend + - specs + - docs diff --git a/openspec/changes/archive/2026-08-26-enhance-brokerage-account-list/design.md b/openspec/changes/archive/2026-08-26-enhance-brokerage-account-list/design.md new file mode 100644 index 0000000..8a016da --- /dev/null +++ b/openspec/changes/archive/2026-08-26-enhance-brokerage-account-list/design.md @@ -0,0 +1,195 @@ +## Context + +`GET /api/v1/brokerage-accounts` уже разрешает текущее T-Bank connection и +поддерживаемые счета через `TbankAccessService`, сохраняя upstream-порядок и +существующее отображение публичных ошибок. Получение Portfolio использует ту +же границу доступа и рассчитывает точные итоги по каждой валюте из значений +позиций `GetPortfolio` и денежных остатков `GetPositions`. Текущая +нормализация также обогащает позиции для экрана деталей счёта, что не нужно +для сводки списка счетов. + +Frontend уже потребляет сгенерированный OpenAPI-тип через один +аутентифицированный React Query hook с 60-секундной политикой актуальности. Его +page VM сохраняет успешный query snapshot во время фонового refetch и выбирает +известные error-состояния списка. Сейчас entity рендерит двухколоночную +коллекцию ссылок только с метаданными счета. Новый контракт должен сохранить +существующую семантику connection, query, навигации и клавиатуры, обогащая +каждый элемент без представления разных валют как единого значения. + +## Goals / Non-Goals + +**Goals:** + +- Расширить существующий контракт списка явной nullable-сводкой Portfolio для + каждого счёта, сохранив policy доступа к счетам и upstream-порядок. +- Переиспользовать точный сохраняющий валюты расчёт итогов Portfolio и для + snapshot деталей, и для сводок списка без работы с метаданными позиций. +- Локализовать временный upstream-сбой на сводке затронутого счёта, сохранив + установленную семантику ошибок connection и token на уровне списка. +- Сохранить существующий 60-секундный React Query-кэш и policy Client Cached + Snapshot, выражая состояния сводки через текущую границу page VM и + presentation. +- Обновить as-built документацию только после проверки поставленного поведения. + +**Non-Goals:** + +- FX-conversion, межвалютный или межсчётный агрегат, отдельное отображение Cash + Balance, сортировка/фильтрация/настройки списка или постоянное хранение + Portfolio. +- Переработка Portfolio на странице деталей счёта, управления T-Bank + connection, resilience policy T-Bank client или глобальной frontend fetch + policy. + +## Decisions + +### D1. Обогатить каждый нормализованный счёт nullable-сводкой на существующей границе списка + +`TbankAccessService` продолжит владеть разрешением current connection, policy +поддерживаемых счетов, обработкой token и начальным вызовом `GetAccounts`. +Поток списка сделает разрешённый token и ID поддерживаемых upstream-счетов +доступными `BrokerageAccountsService`, который сохранит ответственность за +формирование ответа списка. Для каждого поддерживаемого счёта он параллельно +запросит два upstream-входа Portfolio и сохранит нормализованный счёт на его +исходной позиции. + +Работа по каждому счёту будет собрана без изменения порядка счетов. Результат +`TBANK_UNAVAILABLE` любого из входов сводки создаёт `portfolioSummary: null` +только для этого счёта. Начальное разрешение connection, обнаружение счетов, +перевод invalid-token и непредвиденные ошибки сохраняют текущее поведение +Problem Details на уровне списка; они не превращаются молча в частичный успех. + +*Альтернатива:* вызывать public Portfolio service страницы деталей для каждого +счёта. Отклонено, поскольку это свяжет список с cache snapshot деталей, +семантикой timestamp и stale-fallback и вызовет нерелевантное обогащение +метаданных Instrument. + +*Альтернатива:* делать любую неуспешную сводку ошибкой списка. Отклонено, +поскольку это противоречит требуемому поведению частичной доступности и +скрывает в ином случае доступную навигацию к счёту. + +### D2. Извлечь общий normalizer точных итогов, а не вторую модель Portfolio + +Небольшой backend-owned normalizer примет upstream-значения позиций и денежные +остатки и вернёт итоги `Money[]`, сгруппированные по нормализованной исходной +валюте. Он сохранит существующую decimal-арифметику и правила включения: +нулевые значения или значения без валюты исключаются; валидные текущие +значения позиций и Cash Balance в одной валюте точно складываются; +FX-conversion не выполняется. Portfolio service деталей использует helper при +формировании существующего snapshot, а поток списка обернёт тот же результат +как `{ totalsByCurrency }`. + +Это сохраняет денежный инвариант в одном месте и устраняет зависимость от +маппинга позиций Portfolio, запросов метаданных, timestamp или cache-поведения. +Пустой успешный upstream Portfolio становится пустым массивом итогов и, +следовательно, доступной non-null сводкой списка. + +*Альтернатива:* дублировать текущий aggregation loop в service списка. +Отклонено, поскольку две реализации точных денежных расчётов могут разойтись +на edge cases денежных остатков или валют. + +*Альтернатива:* возвращать в списке полный `PortfolioDto`. Отклонено, поскольку +контракт списка не требует ни позиций, ни метаданных snapshot/cache и раскроет +избыточно дорогой payload. + +### D3. Сделать OpenAPI DTO и generated client единственным транспортным изменением + +`BrokerageAccountDto` получает nullable `portfolioSummary`, единственное поле +которой — `totalsByCurrency: MoneyDto[]`. Swagger/OpenAPI описывает эту форму, +после чего frontend snapshot и generated types обновляются установленным +workflow runtime contract. Frontend-код продолжает выводить тип счёта из +сгенерированных `paths`, не создавая параллельный transport interface. + +Это совместимое аддитивное изменение ответа, но оно остаётся изменением +публичного контракта и требует cross-repository gate runtime/OpenAPI. + +### D4. Сохранить query-поведение списка; подготовить представление карточек в page VM + +Существующие query key brokerage-account, 60-секундный `staleTime`, policy без +retry и invalidation после замены/удаления T-Bank connection не изменяются. +Accounts page VM сохранит текущее top-level выделение loading/error/empty/loaded +и поведение cached snapshot. Он подготовит ограниченную display model каждой +карточки из generated account: не более двух итогов исходных валют и явный +счётчик/индикатор остальных валют либо раздельные локальные состояния +недоступной и пустой сводки. + +Presentation component списка счетов станет одноколоночной последовательностью +широких карточек-ссылок. Каждая ссылка сохраняет навигацию по всей карточке и +native keyboard focus и показывает локализованные тип, имя, сводку, вторичные +метаданные даты открытия и видимое действие открытия. Presentation-код +форматирует одно переданное значение Money существующим shared formatter; он +не агрегирует и не конвертирует значения. + +*Альтернатива:* рассчитывать видимые два итога в entity view. Отклонено, +поскольку выбор и подсчёт значений из коллекции — derived presentation data, +принадлежащие page VM согласно frontend VM boundary. + +*Альтернатива:* ввести новый client cache или независимо получать Portfolio +каждого счёта. Отклонено, поскольку обогащённый endpoint списка уже передаёт +нужное компактное наблюдение, а отдельные query добавят fan-out, loading states +и пути invalidation. + +## Delivery Map + +| Area | Status | Architectural outcome or N/A reason | +| --- | --- | --- | +| frontend | `affected` | Regenerated list response type, существующие query policy/invalidation, page VM card display models и доступные одноколоночные summary cards реализуют обогащённые состояния списка. | +| backend | `affected` | Существующая account access boundary поставляет поддерживаемые счета; общий normalizer точных итогов Portfolio и orchestration списка формируют сводки по каждому счёту с локальной обработкой upstream-недоступности и аддитивным DTO. | +| specs | `affected` | Этот change хранит утверждённые Proposal, Delta Specs, Design и последующие implementation Tasks в canonical store. | +| docs | `affected` | После поставки as-built Brokerage Accounts и релевантная system documentation описывают фактический обогащённый контракт списка, частичную доступность сводки и неизменную границу наблюдения current connection. | + +### Contract Gate Decision + +| Gate | Status | Reason | +| --- | --- | --- | +| runtime-openapi | `required` | Backend изменяет публичный ответ списка, а frontend потребляет его generated OpenAPI snapshot. | + +## Risks / Trade-offs + +- **Upstream fan-out растёт с количеством поддерживаемых счетов.** Список + выполняет два Portfolio RPC на счёт, но использует существующие client-wide + rate limiter, deadline и retry policy вместо создания другого механизма + concurrency. Временно недоступная отдельная сводка деградирует локально. +- **Расхождение агрегатов сводки и деталей.** Общая точная нормализация и + фокусированные money tests исключают разную обработку Cash Balance, нулевых + сумм и исходных валют. +- **Частичный ответ может скрыть невалидные учётные данные.** Локальным для + сводки является только известный результат недоступности; token и другие + сбои уровня списка проходят через существующий error translation. +- **Широкие карточки могут стать хуже для сканирования на малых экранах.** + Одноколоночный layout сохраняет компактные вторичные метаданные, ограничивает + отображаемые валюты двумя и явно обозначает пропущенные. +- **Кэшированные данные могут быть временно устаревшими.** Это сохраняет + установленный trade-off 60-секундного Client Cached Snapshot: фоновое + обновление и его предупреждение остаются видимыми, а известные данные не + очищаются. + +## Migration Plan + +1. Добавить компактный summary DTO и разделить точную нормализацию итогов + Portfolio между существующим потоком деталей и обогащённым потоком списка + счетов. +2. Расширить orchestration списка для получения сводок внутри существующей + T-Bank access и resilience boundary, сохранив порядок счетов и отображая в + `null` только недоступность отдельного счёта. +3. Перегенерировать frontend OpenAPI snapshot/types из работающего backend и + пройти runtime contract gate. +4. Адаптировать существующие consumer account query, page VM, карточки, + переводы и risk-based tests, сохранив loading, error, empty, cache и + invalidation behavior. +5. После проверки реализации обновить as-built feature/system documentation + до поставленных контракта и поведения. + +### Rollback Contract + +| Field | Decision | +| --- | --- | +| Status | `required` | +| Reason | Аддитивное поле ответа — публичный API contract, потребляемый frontend. | +| Trigger | Release-blocking defect в доступности списка счетов, точности денежной сводки или совместимости с generated client. | +| Actions | Одновременно откатить frontend consumption и backend additive summary/DTO; перед выпуском откатываемой пары перегенерировать и закоммитить соответствующий frontend OpenAPI snapshot. | +| Data safety | Schema, persisted Portfolio, Brokerage Account или данные владения не создаются и не мутируют; rollback лишь убирает из ответа текущее upstream-наблюдение. | +| Verification | Запустить canonical gates затронутых backend и frontend, а также runtime OpenAPI gate против откатанного контракта; проверить, что список сохраняет прежние метаданные, навигацию и error states. | + +## Open Questions + + diff --git a/openspec/changes/archive/2026-08-26-enhance-brokerage-account-list/proposal.md b/openspec/changes/archive/2026-08-26-enhance-brokerage-account-list/proposal.md new file mode 100644 index 0000000..55ecf1d --- /dev/null +++ b/openspec/changes/archive/2026-08-26-enhance-brokerage-account-list/proposal.md @@ -0,0 +1,52 @@ +## Why + +Страница всех брокерских счетов сейчас помогает только выбрать счёт по имени, +типу и дате открытия. Пользователь не может сравнить текущее состояние счетов +до перехода на каждую отдельную страницу; двухколоночные плитки также не дают +достаточно места для финансовых итогов и плохо поддерживают быстрое +сканирование списка. + +Нужен компактный, но информативный обзор: пользователь должен сравнить счета и +перейти к нужному, не принимая мультивалютные суммы за одну конвертированную +стоимость и не теряя доступ ко всему списку из-за временной проблемы данных +одного счёта. + +## What Changes + +- Расширить наблюдаемый список Brokerage Account текущей сводкой Portfolio по + исходным валютам для каждого доступного счёта. Сводка показывает не более + двух валютных итогов и наличие остальных валют, не выполняя и не подразумевая + FX-конвертацию; Cash Balance не выдаётся за отдельный второй итог. +- Сделать недоступность сводки одного счёта локальным состоянием карточки: + остальные счета и переход к их деталям остаются доступными. +- Переработать экран всех счетов в адаптивный одноколоночный список широких + интерактивных карточек с ясной иерархией: тип, имя, финансовая сводка, + вторичные метаданные и заметное действие открытия счёта. Карточка сохраняет + переход по всей области и доступное клавиатурное состояние. +- Сохранить существующие штатные состояния загрузки, пустого списка и ошибок + списка, включая безопасное отображение последнего успешного клиентского + snapshot при фоновом обновлении. +- **Non-goals:** не добавлять сортировку, фильтры, пользовательские настройки + списка, FX-конвертацию или агрегированный итог по разным счетам; не вводить + долговременное хранение Portfolio или Brokerage Account и не менять страницу + деталей счёта сверх совместимости со списком. + +## Capabilities + +### New Capabilities + +_Нет._ + +### Modified Capabilities + +- `brokerage-accounts`: список счетов и его экран должны предоставлять + сопоставимую мультивалютную сводку Portfolio, устойчиво показывать частичную + недоступность сводки и представлять счета в информативном адаптивном списке. + +## Impact + +Затрагиваются авторизованные пользователи, открывающие `/accounts`, а также +публичный контракт списка Brokerage Account и его frontend-потребитель. +Потребуются синхронные изменения требований, backend-наблюдения данных T-Bank, +frontend-представления и as-built документации; долговременная модель данных, +аутентификация и T-Bank connection остаются вне scope. diff --git a/openspec/changes/archive/2026-08-26-enhance-brokerage-account-list/specs/brokerage-accounts/spec.md b/openspec/changes/archive/2026-08-26-enhance-brokerage-account-list/specs/brokerage-accounts/spec.md new file mode 100644 index 0000000..3237df3 --- /dev/null +++ b/openspec/changes/archive/2026-08-26-enhance-brokerage-account-list/specs/brokerage-accounts/spec.md @@ -0,0 +1,100 @@ +## MODIFIED Requirements + +### Requirement: Список активных брокерских счетов +Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts`, возвращающий массив открытых поддерживаемых инвестиционных счетов T-Bank, доступных через текущий T-Bank connection User, в порядке, полученном от T-Bank. Каждый элемент MUST содержать стабильный `id`, непустое `name`, нормализованный `type` (`BROKERAGE`, `INDIVIDUAL_INVESTMENT`, `INVEST_BOX` или `MONEY_MARKET_FUND`) и nullable `openedOn` в формате `YYYY-MM-DD`. Система MUST включать только счета T-Bank типов «брокерский счёт», «ИИС», «Инвесткопилка» и «фонд денежного рынка»; банковские и неизвестные типы MUST NOT возвращаться. Система MUST удалить внешние пробелы из имени и при пустом результате использовать непустое название соответствующего типа. + +Каждый элемент MUST также содержать nullable `portfolioSummary`. Доступная сводка MUST содержать `totalsByCurrency`: точные Money-итоги Portfolio по исходным ISO-валютам, включающие стоимость позиций и Cash Balance в той же валюте. Она MUST NOT содержать FX-конвертированный или межвалютный итог и MUST NOT выдавать Cash Balance за самостоятельный второй итог. Если сводка Portfolio конкретного доступного счёта временно недоступна, `portfolioSummary` MUST быть `null`, не исключая этот счёт из списка и не делая список неуспешным. Пустой доступный Portfolio MUST передаваться доступной сводкой с пустым `totalsByCurrency`, а не `null`. + +Список Brokerage Account и его сводки являются результатом наблюдения текущего брокерского подключения и MUST NOT создавать ожидание, что WealthLedger хранит долговременное право владения счётом или Portfolio независимо от T-Bank connection. + +#### Scenario: Поддерживаемые счета возвращаются в upstream-порядке +- **WHEN** User с действующим T-Bank connection запрашивает список, а T-Bank возвращает открытые брокерский счёт, ИИС и неподдержанный дебетовый счёт +- **THEN** система отвечает `200` массивом брокерского счёта и ИИС в исходном порядке T-Bank без дебетового счёта + +#### Scenario: Пустой список поддерживаемых счетов +- **WHEN** User с действующим T-Bank connection не имеет открытых поддерживаемых инвестиционных счетов +- **THEN** система отвечает `200` пустым массивом + +#### Scenario: Пустое название получает fallback +- **WHEN** T-Bank возвращает поддерживаемый открытый счёт с пустым либо состоящим из пробелов именем +- **THEN** система отвечает этим счётом с непустым названием его нормализованного типа + +#### Scenario: Дата открытия отсутствует +- **WHEN** T-Bank не возвращает дату открытия поддерживаемого открытого счёта +- **THEN** система отвечает этим счётом с `openedOn: null` + +#### Scenario: Мультивалютная сводка счёта +- **WHEN** доступный Portfolio счёта содержит итоги в RUB и USD, включая Cash Balance в USD +- **THEN** ответ содержит одну доступную `portfolioSummary` с отдельными Money-итогами RUB и USD без FX-конвертации, общего итога или отдельного итога Cash Balance + +#### Scenario: Сводка одного счёта временно недоступна +- **WHEN** список доступных счетов получен, но Portfolio одного из них временно недоступен +- **THEN** система отвечает `200` всеми счетами в исходном порядке, передаёт для этого счёта `portfolioSummary: null` и не заменяет успешные сводки остальных счетов ошибкой списка + +#### Scenario: Доступный Portfolio пуст +- **WHEN** доступный счёт не имеет ненулевых позиций и доступных денежных остатков +- **THEN** система передаёт доступную `portfolioSummary` с пустым `totalsByCurrency` + +### Requirement: Экран списка брокерских счетов +Клиент SHALL загружать список при открытии защищённого маршрута `/accounts` без ручного обновления и без автоматического refetch при возврате фокуса. Клиент MUST использовать успешный frontend-кэш списка в течение 60 секунд. После истечения этого срока клиент MUST запрашивать актуальный список; при уже имеющемся успешном списке он MUST сохранять последний успешный Client Cached Snapshot видимым во время фонового обновления и после его ошибки, ясно отделяя это состояние от свежего результата. При успешном непустом ответе клиент MUST отобразить адаптивный одноколоночный список широких интерактивных карточек счетов с локализованными типом и, если доступна, датой открытия без времени; каждая карточка MUST ясно показывать тип, имя, финансовую сводку, вторичные метаданные и заметное действие открытия счёта, вести на `/accounts/:accountId` по всей области и сохранять доступное клавиатурное состояние. После успешной замены или удаления T-Bank connection клиент MUST инвалидировать список счетов. + +Для доступной `portfolioSummary` карточка MUST показывать не более двух Money-итогов в исходных валютах и обозначать наличие остальных валют, если они есть; она MUST NOT показывать или подразумевать FX-конвертацию, межвалютный агрегированный итог либо Cash Balance как отдельный второй итог. При `portfolioSummary: null` карточка MUST показывать локальное безопасное состояние временной недоступности сводки, сохраняя метаданные счёта и возможность открыть его. При доступной пустой сводке карточка MUST не выдавать отсутствие итогов за временную ошибку. + +#### Scenario: Пользователь открывает счёт из списка +- **WHEN** клиент успешно загрузил непустой список и User выбирает карточку счёта +- **THEN** клиент переходит на `/accounts/:accountId` с `id` выбранного счёта + +#### Scenario: Список пуст +- **WHEN** клиент получает успешный пустой массив +- **THEN** клиент показывает штатное сообщение об отсутствии инвестиционных счетов T-Bank + +#### Scenario: Список берётся из свежего frontend-кэша +- **WHEN** User повторно открывает список в течение 60 секунд после успешной загрузки +- **THEN** клиент показывает сохранённые счета без нового API-запроса + +#### Scenario: Устаревший список загружается повторно +- **WHEN** User открывает список после истечения 60 секунд frontend-кэша и уже существует последний успешный список +- **THEN** клиент оставляет этот список видимым во время нового запроса и ясно обозначает фоновое обновление + +#### Scenario: Ошибка фонового обновления не скрывает snapshot +- **WHEN** обновление устаревшего списка завершается ошибкой при наличии последнего успешного Client Cached Snapshot +- **THEN** клиент сохраняет snapshot видимым, безопасно сообщает о неуспешном обновлении и предоставляет применимое действие повторной загрузки + +#### Scenario: Карточка ограничивает валютные итоги +- **WHEN** доступная сводка счёта содержит итоги в трёх или более валютах +- **THEN** карточка показывает не более двух исходных валютных итогов и обозначает наличие остальных без общего сконвертированного значения + +#### Scenario: Недоступность сводки локальна карточке +- **WHEN** клиент получает список, в котором `portfolioSummary` одного счёта равна `null` +- **THEN** он показывает временную недоступность только на этой карточке, не скрывает остальные карточки и сохраняет переход на детали недоступной сводки счёта + +#### Scenario: Пустая сводка не является ошибкой +- **WHEN** клиент получает доступную `portfolioSummary` с пустым `totalsByCurrency` +- **THEN** он показывает нейтральное состояние отсутствия финансовых итогов, а не состояние временной недоступности + +### Requirement: Клиентские состояния ошибки списка счетов +Клиент MUST NOT автоматически повторять неуспешный запрос списка. При `TBANK_NOT_CONNECTED` клиент SHALL показать объяснение отсутствия подключения и действие «Открыть настройки T-Bank». При `INVALID_TBANK_TOKEN` клиент SHALL показать объяснение недействительного token и то же действие. При `TBANK_UNAVAILABLE` клиент SHALL показать безопасное объяснение временной недоступности, действие повторной загрузки и действие открытия настроек T-Bank. При иной ошибке клиент MUST показать общий безопасный error-state только с действием повторной загрузки. Если последний успешный Client Cached Snapshot отсутствует, эти состояния MUST NOT показывать список; если snapshot существует во время фонового обновления, клиент MUST сохранить его видимым и показать соответствующее безопасное предупреждение и только применимые действия. + +#### Scenario: Подключение требуется +- **WHEN** клиент получает `TBANK_NOT_CONNECTED` +- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank + +#### Scenario: Известная ошибка не повторяется автоматически +- **WHEN** запрос списка завершается известной ошибкой +- **THEN** клиент не выполняет дополнительные запросы, пока User не выберет действие повторной загрузки + +#### Scenario: Token требует внимания +- **WHEN** клиент получает `INVALID_TBANK_TOKEN` +- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank + +#### Scenario: Временная ошибка T-Bank +- **WHEN** клиент получает `TBANK_UNAVAILABLE` +- **THEN** клиент не показывает список и предлагает повторить загрузку либо открыть настройки T-Bank + +#### Scenario: Неизвестная ошибка загрузки +- **WHEN** клиент получает ошибку без известного кода списка счетов +- **THEN** клиент не показывает список и предлагает только повторить загрузку + +#### Scenario: Известная ошибка при сохранённом snapshot +- **WHEN** фоновое обновление списка с последним успешным Client Cached Snapshot получает `TBANK_UNAVAILABLE` +- **THEN** клиент сохраняет snapshot видимым, сообщает о временной недоступности и предлагает повторить загрузку либо открыть настройки T-Bank diff --git a/openspec/changes/archive/2026-08-26-enhance-brokerage-account-list/tasks.md b/openspec/changes/archive/2026-08-26-enhance-brokerage-account-list/tasks.md new file mode 100644 index 0000000..647f790 --- /dev/null +++ b/openspec/changes/archive/2026-08-26-enhance-brokerage-account-list/tasks.md @@ -0,0 +1,50 @@ +## 1. Точная backend-сводка и контракт списка + +- [x] 1.1 [backend] Извлечь backend-owned normalizer `Money[]` из текущего Portfolio flow и применить его как для snapshot деталей, так и для компактной сводки списка: нормализовать ISO-валюты, точно сложить ненулевые стоимости позиций и Cash Balance одной валюты, не выполнять FX-конвертацию и не загружать Instrument metadata для списка. + - **Covers:** `brokerage-accounts` — `Список активных брокерских счетов` + - **Verification:** `npm test -- tbank/portfolio.service.spec.ts --runInBand` в `backend` с cases точных итогов, Cash Balance, нулевых/безвалютных значений и пустого доступного Portfolio. +- [x] 1.2 [backend] Расширить `BrokerageAccountDto` nullable `portfolioSummary` с `totalsByCurrency` и изменить list orchestration за существующей `TbankAccessService`: для каждого поддерживаемого upstream-счёта параллельно получить два Portfolio-входа, сохранить upstream-порядок, локализовать только `TBANK_UNAVAILABLE` как `portfolioSummary: null`, а ошибки connection/token и прочие ошибки списка оставить существующей boundary. + - **Covers:** `brokerage-accounts` — `Список активных брокерских счетов` + - **Verification:** `npm test -- tbank/brokerage-accounts.service.spec.ts --runInBand` в `backend` с cases порядка, успешных нескольких сводок, локальной unavailable-сводки, пустой сводки и неподавленных connection/token ошибок. +- [x] 1.3 [backend] Пройти canonical backend gate после DTO, normalizer и list orchestration. + - **Covers:** `N/A` — canonical gate проверяет реализацию уже покрытого contract и money behavior, не вводя отдельного observable requirement. + - **Verification:** `npm run verify` в `backend` под Node 22. + +## 2. Generated frontend contract и доступный список + +- [x] 2.1 [frontend] После доступности backend OpenAPI синхронизировать committed OpenAPI snapshot и generated schema через штатный workflow; использовать расширенный generated `BrokerageAccount` type без ручного transport DTO. + - **Covers:** `N/A` — это обязательная реализация решения D3 для already-specified public response, не новое client behavior. + - **Verification:** `npm run openapi:sync` в `frontend`, затем `npm run openapi:check` в `frontend`. +- [x] 2.2 [frontend] Расширить Accounts page VM и её focused tests: сохранить существующие key, `staleTime: 60_000`, отсутствие retry/refetch-on-focus, invalidation и top-level loading/error/empty/cached-snapshot selection; подготовить display model сводки карточки с максимум двумя исходными Money, индикатором остальных валют и раздельными состояниями unavailable/empty. + - **Covers:** `brokerage-accounts` — `Экран списка брокерских счетов` + - **Verification:** `npm run test:run -- src/pages/accounts/ui/AccountsPage.test.tsx` в `frontend` с cases fresh cache, background refresh/error snapshot, limit двух валют, remaining-currency indicator, unavailable и empty summary. +- [x] 2.3 [frontend] Переработать presentation списка в адаптивную одноколоночную последовательность широких ссылок-карточек: отобразить локализованные тип, имя, summary, вторичные метаданные даты и видимое действие открытия; сохранить native link navigation/focus, безопасный local summary state и форматирование каждого переданного Money существующим formatter без агрегации или FX-смысла. + - **Covers:** `brokerage-accounts` — `Экран списка брокерских счетов` + - **Verification:** `npm run test:run -- src/entities/brokerage-account/ui/BrokerageAccountList.test.tsx` в `frontend` с accessible link/action, доступными summary states и responsive single-column class behavior. +- [x] 2.4 [frontend] Сохранить известные list error states и recovery actions в page VM/presentation: connection и invalid-token ведут только к настройкам, temporary unavailable показывает retry и settings, unknown error — только retry; при Client Cached Snapshot warning не скрывает карточки и не запускает retry автоматически. + - **Covers:** `brokerage-accounts` — `Клиентские состояния ошибки списка счетов` + - **Verification:** `npm run test:run -- src/pages/accounts/ui/AccountsPage.test.tsx` в `frontend` с cases всех известных/unknown errors, recovery actions и cached-snapshot warning. +- [x] 2.5 [frontend] Пройти canonical frontend gate после generated contract, VM, presentation и tests. + - **Covers:** `N/A` — canonical gate подтверждает delivered frontend behavior и boundary rules, но не задаёт новый requirement. + - **Verification:** `npm run verify` в `frontend` под Node 22. + +## 3. As-built documentation и canonical SDD checks + +- [x] 3.1 [docs] Обновить delivered documentation брокерских счетов и system overview после verified implementation: описать additive summary contract, исходные валюты без FX-агрегации, локальную unavailable-сводку, неизменную current-connection boundary и Client Cached Snapshot; не копировать OpenSpec planning artifacts. + - **Covers:** `N/A` — docs фиксирует только поставленное as-built состояние из Design Delivery Map и не создаёт самостоятельного observable behavior. + - **Verification:** Review `docs/features/brokerage-accounts.md` и `docs/architecture/system-overview.md` against delivered backend OpenAPI and frontend behavior. +- [x] 3.2 [docs] Пройти canonical docs gate для as-built documentation. + - **Covers:** `N/A` — Markdown gate подтверждает documentation delivery и не реализует отдельный Delta Requirement. + - **Verification:** `npm run verify` в `docs` под Node 22. +- [x] 3.3 [specs] Проверить canonical change artifacts и strict Delta Specs после создания Tasks, не создавая competing planning source. + - **Covers:** `N/A` — specs area владеет SDD artifact integrity, а не отдельным runtime behavior. + - **Verification:** `bash specs/scripts/verify-openspec-planning.sh enhance-brokerage-account-list`, `openspec validate enhance-brokerage-account-list --type change --strict --store wealth-ledger-specs` и `openspec validate --specs --strict --store wealth-ledger-specs` из `specs`. + +## 4. Cross-repository verification before archive + +- [x] 4.1 [workspace] Выполнить canonical workspace dispatcher после всех repository-owned tasks, включая required runtime OpenAPI contract gate для backend response и committed frontend snapshot. + - **Covers:** `N/A` — workspace только оркестрирует approved Delivery Map и repository gates; он не владеет implementation behavior. + - **Verification:** `bash specs/scripts/verify-workspace-change.sh enhance-brokerage-account-list` из workspace root под Node 22. +- [x] 4.2 [workspace] Провести semantic verification и independent read-only review implementation против Proposal, всех Delta Requirements/Scenarios, Decisions D1–D4, rollback contract и completed Tasks; устранить Critical и Important findings до archive eligibility. + - **Covers:** `N/A` — terminal review доказывает соответствие уже specified behavior, не вводя нового requirement. + - **Verification:** Сохранить review scope, findings и resolution только после фактической проверки; подтвердить отсутствие Critical и Important findings. diff --git a/openspec/specs/brokerage-accounts/spec.md b/openspec/specs/brokerage-accounts/spec.md index 38bf07c..7f74a4a 100644 --- a/openspec/specs/brokerage-accounts/spec.md +++ b/openspec/specs/brokerage-accounts/spec.md @@ -9,7 +9,9 @@ ### Requirement: Список активных брокерских счетов Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts`, возвращающий массив открытых поддерживаемых инвестиционных счетов T-Bank, доступных через текущий T-Bank connection User, в порядке, полученном от T-Bank. Каждый элемент MUST содержать стабильный `id`, непустое `name`, нормализованный `type` (`BROKERAGE`, `INDIVIDUAL_INVESTMENT`, `INVEST_BOX` или `MONEY_MARKET_FUND`) и nullable `openedOn` в формате `YYYY-MM-DD`. Система MUST включать только счета T-Bank типов «брокерский счёт», «ИИС», «Инвесткопилка» и «фонд денежного рынка»; банковские и неизвестные типы MUST NOT возвращаться. Система MUST удалить внешние пробелы из имени и при пустом результате использовать непустое название соответствующего типа. -Список Brokerage Account является результатом наблюдения текущего брокерского подключения и MUST NOT создавать ожидание, что WealthLedger хранит долговременное право владения счётом независимо от T-Bank connection. +Каждый элемент MUST также содержать nullable `portfolioSummary`. Доступная сводка MUST содержать `totalsByCurrency`: точные Money-итоги Portfolio по исходным ISO-валютам, включающие стоимость позиций и Cash Balance в той же валюте. Она MUST NOT содержать FX-конвертированный или межвалютный итог и MUST NOT выдавать Cash Balance за самостоятельный второй итог. Если сводка Portfolio конкретного доступного счёта временно недоступна, `portfolioSummary` MUST быть `null`, не исключая этот счёт из списка и не делая список неуспешным. Пустой доступный Portfolio MUST передаваться доступной сводкой с пустым `totalsByCurrency`, а не `null`. + +Список Brokerage Account и его сводки являются результатом наблюдения текущего брокерского подключения и MUST NOT создавать ожидание, что WealthLedger хранит долговременное право владения счётом или Portfolio независимо от T-Bank connection. #### Scenario: Поддерживаемые счета возвращаются в upstream-порядке - **WHEN** User с действующим T-Bank connection запрашивает список, а T-Bank возвращает открытые брокерский счёт, ИИС и неподдержанный дебетовый счёт @@ -27,6 +29,18 @@ - **WHEN** T-Bank не возвращает дату открытия поддерживаемого открытого счёта - **THEN** система отвечает этим счётом с `openedOn: null` +#### Scenario: Мультивалютная сводка счёта +- **WHEN** доступный Portfolio счёта содержит итоги в RUB и USD, включая Cash Balance в USD +- **THEN** ответ содержит одну доступную `portfolioSummary` с отдельными Money-итогами RUB и USD без FX-конвертации, общего итога или отдельного итога Cash Balance + +#### Scenario: Сводка одного счёта временно недоступна +- **WHEN** список доступных счетов получен, но Portfolio одного из них временно недоступен +- **THEN** система отвечает `200` всеми счетами в исходном порядке, передаёт для этого счёта `portfolioSummary: null` и не заменяет успешные сводки остальных счетов ошибкой списка + +#### Scenario: Доступный Portfolio пуст +- **WHEN** доступный счёт не имеет ненулевых позиций и доступных денежных остатков +- **THEN** система передаёт доступную `portfolioSummary` с пустым `totalsByCurrency` + ### Requirement: Состояния недоступности списка счетов Система MUST NOT возвращать частичный или сохранённый ранее список счетов. При отсутствии T-Bank connection система MUST отвечать `409` Problem Details с code `TBANK_NOT_CONNECTED`. Если T-Bank отклоняет сохранённый token как неаутентифицированный или без прав, система MUST отвечать `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохранять connection. При иной неуспешной попытке получить список из T-Bank система MUST отвечать `503` Problem Details с code `TBANK_UNAVAILABLE`. @@ -43,7 +57,9 @@ - **THEN** система отвечает `503` Problem Details с code `TBANK_UNAVAILABLE` без счетов ### Requirement: Экран списка брокерских счетов -Клиент SHALL загружать список при открытии защищённого маршрута `/accounts` без ручного обновления и без автоматического refetch при возврате фокуса. Клиент MUST использовать успешный frontend-кэш списка в течение 60 секунд; после истечения этого срока при новом запросе клиент MUST скрывать ранее показанный список и показывать loading-состояние. При успешном непустом ответе клиент MUST отобразить светлые интерактивные карточки счетов с названием, локализованным типом и, если доступна, локализованной датой открытия без времени; карточки MUST вести на `/accounts/:accountId`. При успешном пустом ответе клиент MUST показать штатное состояние отсутствия инвестиционных счетов. После успешной замены или удаления T-Bank connection клиент MUST инвалидировать список счетов. +Клиент SHALL загружать список при открытии защищённого маршрута `/accounts` без ручного обновления и без автоматического refetch при возврате фокуса. Клиент MUST использовать успешный frontend-кэш списка в течение 60 секунд. После истечения этого срока клиент MUST запрашивать актуальный список; при уже имеющемся успешном списке он MUST сохранять последний успешный Client Cached Snapshot видимым во время фонового обновления и после его ошибки, ясно отделяя это состояние от свежего результата. При успешном непустом ответе клиент MUST отобразить адаптивный одноколоночный список широких интерактивных карточек счетов с локализованными типом и, если доступна, датой открытия без времени; каждая карточка MUST ясно показывать тип, имя, финансовую сводку, вторичные метаданные и заметное действие открытия счёта, вести на `/accounts/:accountId` по всей области и сохранять доступное клавиатурное состояние. После успешной замены или удаления T-Bank connection клиент MUST инвалидировать список счетов. + +Для доступной `portfolioSummary` карточка MUST показывать не более двух Money-итогов в исходных валютах и обозначать наличие остальных валют, если они есть; она MUST NOT показывать или подразумевать FX-конвертацию, межвалютный агрегированный итог либо Cash Balance как отдельный второй итог. При `portfolioSummary: null` карточка MUST показывать локальное безопасное состояние временной недоступности сводки, сохраняя метаданные счёта и возможность открыть его. При доступной пустой сводке карточка MUST не выдавать отсутствие итогов за временную ошибку. #### Scenario: Пользователь открывает счёт из списка - **WHEN** клиент успешно загрузил непустой список и User выбирает карточку счёта @@ -58,11 +74,27 @@ - **THEN** клиент показывает сохранённые счета без нового API-запроса #### Scenario: Устаревший список загружается повторно -- **WHEN** User открывает список после истечения 60 секунд frontend-кэша -- **THEN** клиент не показывает ранее загруженные счета до получения нового результата +- **WHEN** User открывает список после истечения 60 секунд frontend-кэша и уже существует последний успешный список +- **THEN** клиент оставляет этот список видимым во время нового запроса и ясно обозначает фоновое обновление + +#### Scenario: Ошибка фонового обновления не скрывает snapshot +- **WHEN** обновление устаревшего списка завершается ошибкой при наличии последнего успешного Client Cached Snapshot +- **THEN** клиент сохраняет snapshot видимым, безопасно сообщает о неуспешном обновлении и предоставляет применимое действие повторной загрузки + +#### Scenario: Карточка ограничивает валютные итоги +- **WHEN** доступная сводка счёта содержит итоги в трёх или более валютах +- **THEN** карточка показывает не более двух исходных валютных итогов и обозначает наличие остальных без общего сконвертированного значения + +#### Scenario: Недоступность сводки локальна карточке +- **WHEN** клиент получает список, в котором `portfolioSummary` одного счёта равна `null` +- **THEN** он показывает временную недоступность только на этой карточке, не скрывает остальные карточки и сохраняет переход на детали недоступной сводки счёта + +#### Scenario: Пустая сводка не является ошибкой +- **WHEN** клиент получает доступную `portfolioSummary` с пустым `totalsByCurrency` +- **THEN** он показывает нейтральное состояние отсутствия финансовых итогов, а не состояние временной недоступности ### Requirement: Клиентские состояния ошибки списка счетов -Клиент MUST NOT автоматически повторять неуспешный запрос списка. При `TBANK_NOT_CONNECTED` клиент SHALL показать объяснение отсутствия подключения и действие «Открыть настройки T-Bank». При `INVALID_TBANK_TOKEN` клиент SHALL показать объяснение недействительного token и то же действие. При `TBANK_UNAVAILABLE` клиент SHALL показать безопасное объяснение временной недоступности, действие повторной загрузки и действие открытия настроек T-Bank. При иной ошибке клиент MUST показать общий безопасный error-state только с действием повторной загрузки. +Клиент MUST NOT автоматически повторять неуспешный запрос списка. При `TBANK_NOT_CONNECTED` клиент SHALL показать объяснение отсутствия подключения и действие «Открыть настройки T-Bank». При `INVALID_TBANK_TOKEN` клиент SHALL показать объяснение недействительного token и то же действие. При `TBANK_UNAVAILABLE` клиент SHALL показать безопасное объяснение временной недоступности, действие повторной загрузки и действие открытия настроек T-Bank. При иной ошибке клиент MUST показать общий безопасный error-state только с действием повторной загрузки. Если последний успешный Client Cached Snapshot отсутствует, эти состояния MUST NOT показывать список; если snapshot существует во время фонового обновления, клиент MUST сохранить его видимым и показать соответствующее безопасное предупреждение и только применимые действия. #### Scenario: Подключение требуется - **WHEN** клиент получает `TBANK_NOT_CONNECTED` @@ -83,3 +115,7 @@ #### Scenario: Неизвестная ошибка загрузки - **WHEN** клиент получает ошибку без известного кода списка счетов - **THEN** клиент не показывает список и предлагает только повторить загрузку + +#### Scenario: Известная ошибка при сохранённом snapshot +- **WHEN** фоновое обновление списка с последним успешным Client Cached Snapshot получает `TBANK_UNAVAILABLE` +- **THEN** клиент сохраняет snapshot видимым, сообщает о временной недоступности и предлагает повторить загрузку либо открыть настройки T-Bank