Files
specs/openspec/changes/archive/2026-08-26-enhance-brokerage-account-list/design.md
T

196 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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
<!-- none -->