docs(specs): archive stock ticker change
This commit is contained in:
+2
@@ -0,0 +1,2 @@
|
||||
schema: wealth-ledger-sdd
|
||||
created: 2026-08-27
|
||||
@@ -0,0 +1,102 @@
|
||||
## Context
|
||||
|
||||
Portfolio уже нормализуется backend-ом из финансовых данных и metadata
|
||||
инструмента, после чего frontend получает только OpenAPI-контракт. Metadata
|
||||
разрешается по instrument UID с fallback на FIGI и уже содержит тикер для
|
||||
построения отображаемого имени, однако тикер не входит в нормализованную
|
||||
Portfolio Position. Карточка позиции отображает ISIN независимо от типа
|
||||
инструмента.
|
||||
|
||||
Изменение расширяет публичный ответ Portfolio nullable presentation metadata и
|
||||
затрагивает обе стороны committed OpenAPI-контракта. Денежные значения,
|
||||
расчёты, доступ к счёту, metadata lookup и cache policies остаются прежними.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Передавать доступный тикер через нормализованную Portfolio Position без
|
||||
отдельного источника данных на клиенте.
|
||||
- Отображать на карточке `STOCK` тикер либо ISIN как определённый fallback.
|
||||
- Сохранить единый backend-owned API contract и generated frontend types.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Не менять правила формирования отображаемого имени, порядок, секции или
|
||||
финансовые данные Portfolio.
|
||||
- Не менять представление облигаций и иных типов позиций.
|
||||
- Не вводить новый upstream lookup, cache, endpoint, хранилище или клиентскую
|
||||
интеграцию с T-Bank.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Nullable ticker остаётся metadata Portfolio Position
|
||||
|
||||
Backend добавляет nullable `ticker` к нормализованной Position и заполняет его
|
||||
из уже разрешённой metadata инструмента. Пустое значение и отсутствие metadata
|
||||
нормализуются в `null`, как существующие presentation metadata. Это делает
|
||||
данные доступными одинаково для всех типов Position, не меняет уже принятый
|
||||
порядок разрешения metadata и не влияет на финансовый Snapshot.
|
||||
|
||||
Не использовать тикер непосредственно из исходной Portfolio Position: это
|
||||
обошло бы единый UID/FIGI resolution path и могло бы расходиться с другими
|
||||
metadata полями. Не запрашивать metadata на frontend: это нарушило бы
|
||||
server-side boundary T-Bank и создало бы второй источник контрактных данных.
|
||||
|
||||
### OpenAPI — единственный межрепозиторный шов
|
||||
|
||||
DTO backend-а объявляет nullable `ticker`; опубликованный runtime OpenAPI
|
||||
документ становится источником для committed frontend snapshot и generated
|
||||
types. Frontend не создаёт ручной transport type и не меняет контракт локально.
|
||||
Так additive field остаётся синхронным между runtime API и клиентом.
|
||||
|
||||
### Выбор идентификатора принадлежит presentation layer карточки
|
||||
|
||||
Существующая VM продолжает передавать structural Portfolio Position из
|
||||
generated контракта. Карточка определяет подпись только из уже переданных
|
||||
`type`, `ticker` и `isin`: для `STOCK` выбирает `ticker ?? isin`, для остальных
|
||||
типов сохраняет `isin`. Это локальная presentation-логика одного значения,
|
||||
поэтому не переносит в view fetching, группировку или финансовые вычисления.
|
||||
|
||||
## Delivery Map
|
||||
|
||||
| Area | Status | Architectural outcome or N/A reason |
|
||||
| --- | --- | --- |
|
||||
| frontend | `affected` | Получает синхронизированные generated types и отображает на карточке `STOCK` тикер с ISIN fallback, не меняя карточки иных типов. |
|
||||
| backend | `affected` | Нормализует nullable ticker из существующей metadata и публикует его в Portfolio API/OpenAPI. |
|
||||
| specs | `affected` | Delta `portfolio-view` закрепляет nullable ticker и type-specific presentation fallback. |
|
||||
| docs | `affected` | После проверенной реализации feature documentation Portfolio описывает поле ticker и stock-only правило отображения. |
|
||||
|
||||
### Contract Gate Decision
|
||||
|
||||
| Gate | Status | Reason |
|
||||
| --- | --- | --- |
|
||||
| runtime-openapi | `required` | Backend публикует additive поле Portfolio API, а frontend потребляет его через committed OpenAPI snapshot и generated types. |
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- Ticker остаётся best-effort metadata; nullable contract и ISIN fallback не
|
||||
скрывают Position при его отсутствии.
|
||||
- Additive публичное поле требует синхронной поставки runtime OpenAPI и
|
||||
committed frontend snapshot; contract gate предотвращает их расхождение.
|
||||
- Неэквивалентность тикера и отображаемого имени намеренна: имя сохраняет
|
||||
существующий приоритет, тикер служит компактной подписью только акции.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Расширить backend-нормализацию и публичный contract nullable ticker без
|
||||
изменения существующих полей или cache semantics.
|
||||
2. Синхронизировать generated frontend contract с runtime OpenAPI и применить
|
||||
type-specific presentation rule в карточке.
|
||||
3. Обновить as-built Portfolio documentation после подтверждения delivered
|
||||
поведения.
|
||||
|
||||
### Rollback Contract
|
||||
|
||||
| Field | Decision |
|
||||
| --- | --- |
|
||||
| Status | `required` |
|
||||
| Trigger | Обнаружено, что новое поле или его отображение нарушает совместимость Portfolio API либо неправильно идентифицирует акции. |
|
||||
| Actions | Сначала вернуть frontend-карточки акций к ISIN. Nullable `ticker` не удалять из backend публичного ответа во время rollback, чтобы не ломать уже синхронизированный клиентский contract; последующее удаление возможно только отдельным compatibility change. |
|
||||
| Data safety | Тикер — производная metadata текущего in-memory Portfolio Snapshot; миграций, persistent data и денежных расчётов нет. |
|
||||
| Verification | Подтвердить сохранение ISIN на stock-карточках и соответствие runtime OpenAPI committed frontend snapshot. |
|
||||
@@ -0,0 +1,35 @@
|
||||
## Why
|
||||
|
||||
В карточке акции под названием инструмента сейчас показывается ISIN — длинный
|
||||
международный идентификатор, который неудобен для быстрого распознавания
|
||||
позиции. Пользователь ожидает видеть привычный биржевой тикер, например `LKOH`
|
||||
для ЛУКОЙЛ.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Для каждой позиции Portfolio сохранить доступный тикер инструмента наряду с
|
||||
существующими данными.
|
||||
- На карточке позиции типа `STOCK` показывать только тикер под названием
|
||||
инструмента.
|
||||
- Если для акции тикер недоступен, сохранять отображение её ISIN как fallback.
|
||||
- Не менять карточки облигаций и других типов позиций, порядок позиций,
|
||||
финансовые значения, агрегаты Portfolio или навигацию.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
Нет.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `portfolio-view`: Portfolio Position предоставляет ticker, а карточка акции
|
||||
предпочитает его ISIN с определённым fallback.
|
||||
|
||||
## Impact
|
||||
|
||||
- Пользователь быстрее распознаёт акции в Portfolio по привычному биржевому
|
||||
тикеру.
|
||||
- Затрагиваются данные позиции Portfolio и представление карточки акции на
|
||||
странице счёта; остальные карточки и данные Portfolio сохраняют текущее
|
||||
поведение.
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Нормализованный состав Portfolio
|
||||
Система MUST возвращать только обычные Portfolio Position с ненулевым количеством и доступные Cash Balance с ненулевой суммой. Cash Balance MUST быть отдельным от Portfolio Position элементом и агрегироваться до одного значения на валюту. Portfolio Position с типом `CURRENCY` MUST означать позицию по торгуемому валютному Instrument и MUST NOT смешиваться с Cash Balance. Порядок Portfolio Position и Cash Balance MUST соответствовать upstream-порядку; система MUST NOT сортировать или объединять Portfolio Position с одинаковым FIGI. Виртуальные позиции и заблокированные денежные остатки MUST NOT входить в Portfolio.
|
||||
|
||||
Каждая Portfolio Position MUST содержать нормализованный тип `STOCK`, `BOND`, `FUND`, `CURRENCY`, `FUTURE`, `OPTION`, `STRUCTURED_NOTE`, `DIGITAL_FINANCIAL_ASSET` либо `OTHER`, строковое `quantity`, nullable `currentValue`, nullable `averagePurchasePrice`, nullable `absoluteReturn`, nullable `isin`, nullable `ticker` и nullable `logoName` — имя файла логотипа бренда инструмента. Имя файла логотипа MUST заполняться из metadata инструмента, когда upstream предоставляет бренд с непустым именем файла логотипа; во всех остальных случаях оно MUST быть `null`. Логотип является presentation metadata: он не влияет на количество, стоимость, агрегаты, успешность или свежесть Portfolio Snapshot. Система MUST искать metadata по instrument UID при его наличии, затем по FIGI. Отображаемый идентификатор MUST предпочитать name, затем ticker, а при недоступности metadata использовать FIGI, не скрывая финансовые значения.
|
||||
|
||||
#### Scenario: Позиция с неизвестным upstream-типом
|
||||
- **WHEN** upstream возвращает обычную ненулевую позицию с нераспознанным типом инструмента
|
||||
- **THEN** система возвращает её в исходном порядке с типом `OTHER`
|
||||
|
||||
#### Scenario: Позиция валютного инструмента не является денежным остатком
|
||||
- **WHEN** upstream возвращает ненулевую Portfolio Position валютного инструмента и ненулевой Cash Balance той же валюты
|
||||
- **THEN** система возвращает Portfolio Position с типом `CURRENCY` отдельно от Cash Balance и не объединяет их между собой
|
||||
|
||||
#### Scenario: Позиция без metadata инструмента
|
||||
- **WHEN** lookup metadata по FIGI не дал ticker или name либо временно недоступен
|
||||
- **THEN** система возвращает позицию с FIGI как fallback-идентификатором и всеми доступными финансовыми значениями
|
||||
|
||||
#### Scenario: Имя инструмента доступно
|
||||
- **WHEN** metadata инструмента по FIGI содержит непустое name
|
||||
- **THEN** система возвращает name как отображаемый идентификатор позиции вместо FIGI или ticker
|
||||
|
||||
#### Scenario: FIGI не разрешается, но доступен instrument UID
|
||||
- **WHEN** FIGI позиции не даёт metadata, а её instrument UID возвращает непустое name
|
||||
- **THEN** система возвращает name, полученное по instrument UID
|
||||
|
||||
#### Scenario: ISIN инструмента доступен
|
||||
- **WHEN** metadata инструмента позиции содержит непустой `isin`
|
||||
- **THEN** система возвращает его в поле `isin` позиции
|
||||
|
||||
#### Scenario: ISIN инструмента отсутствует
|
||||
- **WHEN** у инструмента позиции нет ISIN либо metadata недоступна
|
||||
- **THEN** система возвращает позицию с `isin: null`, не скрывая финансовые значения
|
||||
|
||||
#### Scenario: Тикер инструмента доступен
|
||||
- **WHEN** metadata инструмента позиции содержит непустой `ticker`
|
||||
- **THEN** система возвращает его в поле `ticker` позиции
|
||||
|
||||
#### Scenario: Тикер инструмента отсутствует
|
||||
- **WHEN** у инструмента позиции нет ticker либо metadata недоступна
|
||||
- **THEN** система возвращает позицию с `ticker: null`, не скрывая финансовые значения
|
||||
|
||||
#### Scenario: Имя файла логотипа бренда доступно
|
||||
- **WHEN** metadata инструмента позиции содержит бренд с непустым именем файла логотипа
|
||||
- **THEN** система возвращает его в поле `logoName` позиции
|
||||
|
||||
#### Scenario: Имя файла логотипа бренда отсутствует
|
||||
- **WHEN** у инструмента позиции нет бренда, имя файла логотипа пустое либо metadata недоступна
|
||||
- **THEN** система возвращает позицию с `logoName: null`, не скрывая финансовые значения
|
||||
|
||||
#### Scenario: Денежные остатки в одной валюте
|
||||
- **WHEN** upstream возвращает несколько ненулевых доступных денежных остатков одной валюты
|
||||
- **THEN** система возвращает один Cash Balance с их точной суммой
|
||||
|
||||
#### Scenario: Штатный пустой Portfolio
|
||||
- **WHEN** у доступного Brokerage Account нет ненулевых обычных позиций и доступных денежных остатков
|
||||
- **THEN** система отвечает `200` с пустыми `positions`, `cashBalances` и `totalsByCurrency`
|
||||
|
||||
### Requirement: Представление карточек позиций на клиенте
|
||||
Клиент MUST отображать `STOCK` и `BOND` отдельными секциями. Внутри каждой секции MUST сохраняться upstream-порядок позиций. Позиции иных типов MUST оставаться доступными в отдельной общей секции.
|
||||
|
||||
Карточки секций `STOCK` и `BOND` MUST отображаться по одной в ряду при любой ширине экрана. Карточка акции или облигации MUST показывать визуальный логотип бренда инструмента рядом с отображаемым идентификатором позиции; при отсутствии имени файла логотипа либо неудачной загрузке изображения вместо логотипа MUST отображаться первая буква отображаемого идентификатора позиции. Абсолютный доход в карточке акции или облигации MUST отображаться значением с явным знаком (`+` для положительного, `-` для отрицательного, без знака при нуле) с цветовой индикацией знака и без текстовой подписи доходности.
|
||||
|
||||
Карточка `STOCK` MUST показывать только `ticker` под отображаемым идентификатором позиции, если тикер доступен; при `ticker: null` она MUST сохранять текущее отображение `isin` как fallback. Карточки `BOND` и общей секции иных типов MUST сохранять отображение `isin`. Карточки общей секции иных типов MUST сохранять прежнее поведение: компактная плитка в адаптивном grid (одна колонка на узком экране, несколько колонок на широком), абсолютный доход с текстовой подписью доходности и цветовой индикацией знака, без визуального логотипа. Плитки всех секций MUST показывать отображаемый идентификатор позиции, количество, `currentValue` как главную метрику, `absoluteReturn`, `averagePurchasePrice` и долю позиции среди позиций той же валюты. Отсутствующие метрики MUST быть скрыты, а не отображаться заглушкой. При форматировании денежных значений в русской локали клиент MUST отображать RUB как символ `₽`; другие валюты MUST сохранять понятное обозначение.
|
||||
|
||||
#### Scenario: Отдельные акции и облигации
|
||||
- **WHEN** Portfolio содержит позиции `STOCK` и `BOND`
|
||||
- **THEN** клиент отображает их в отдельных секциях «Акции» и «Облигации» без изменения порядка в каждой секции
|
||||
|
||||
#### Scenario: Портфель отображается на мобильном устройстве
|
||||
- **WHEN** User открывает загруженный Portfolio на узком экране
|
||||
- **THEN** клиент сохраняет читаемое одноколоночное отображение агрегатов, позиций и Cash Balance
|
||||
|
||||
#### Scenario: Карточка акции с доступным тикером
|
||||
- **WHEN** позиция `STOCK` имеет непустой `ticker` и ISIN
|
||||
- **THEN** клиент показывает под отображаемым идентификатором только тикер и не показывает ISIN
|
||||
|
||||
#### Scenario: Карточка акции без тикера
|
||||
- **WHEN** позиция `STOCK` имеет `ticker: null` и непустой ISIN
|
||||
- **THEN** клиент показывает ISIN под отображаемым идентификатором
|
||||
|
||||
#### Scenario: Карточка облигации сохраняет ISIN
|
||||
- **WHEN** позиция `BOND` имеет доступные ticker и ISIN
|
||||
- **THEN** клиент показывает ISIN и не заменяет его тикером
|
||||
|
||||
#### Scenario: Карточка позиции отображает доступные метрики
|
||||
- **WHEN** позиция, не относящаяся к `STOCK`, имеет значение текущей стоимости, абсолютного дохода, средней цены покупки и ISIN
|
||||
- **THEN** клиент показывает идентификатор, количество, текущую стоимость, абсолютный доход, среднюю цену покупки, долю позиции среди позиций той же валюты и ISIN
|
||||
|
||||
#### Scenario: Отсутствующие метрики скрыты
|
||||
- **WHEN** у позиции отсутствует значение текущей стоимости, средней цены покупки или дохода
|
||||
- **THEN** клиент скрывает соответствующую метрику вместо отображения заглушки
|
||||
|
||||
#### Scenario: Доход окрашен по знаку
|
||||
- **WHEN** у позиции абсолютный доход положительный или отрицательный
|
||||
- **THEN** клиент отображает его зелёным для положительного и красным для отрицательного, без цвета при его отсутствии
|
||||
|
||||
#### Scenario: Доля позиции в валюте
|
||||
- **WHEN** позиция имеет вычислимую текущую стоимость в валюте
|
||||
- **THEN** клиент отображает её долю среди всех позиций той же валюты, не включая Cash Balance
|
||||
|
||||
#### Scenario: Одноколоночные секции акций и облигаций
|
||||
- **WHEN** User открывает загруженный Portfolio с позициями `STOCK` или `BOND` на широком экране
|
||||
- **THEN** клиент отображает карточки соответствующей секции по одной в ряду
|
||||
|
||||
#### Scenario: Логотип акции или облигации отображается
|
||||
- **WHEN** позиция `STOCK` или `BOND` содержит непустое имя файла логотипа бренда и изображение успешно загружается
|
||||
- **THEN** клиент показывает изображение логотипа рядом с отображаемым идентификатором позиции
|
||||
|
||||
#### Scenario: Фолбэк без логотипа у акции или облигации
|
||||
- **WHEN** у позиции `STOCK` или `BOND` имя файла логотипа отсутствует либо изображение не загружается
|
||||
- **THEN** вместо логотипа клиент показывает первую букву отображаемого идентификатора позиции
|
||||
|
||||
#### Scenario: Доходность акции или облигации со знаком без подписи
|
||||
- **WHEN** карточка акции или облигации показывает абсолютный доход
|
||||
- **THEN** клиент отображает значение с явным знаком `+` или `-` (без знака при нуле) и без текстовой подписи доходности
|
||||
|
||||
#### Scenario: Секция облигаций в едином стиле с акциями
|
||||
- **WHEN** User открывает загруженный Portfolio с позициями `BOND`
|
||||
- **THEN** клиент отображает облигации по одной карточке в ряду с визуальным логотипом бренда либо фолбэком первой буквы и абсолютным доходом с явным знаком без текстовой подписи
|
||||
|
||||
#### Scenario: Секция иных типов сохраняет прежний вид
|
||||
- **WHEN** User открывает загруженный Portfolio с позициями иных типов (не `STOCK` и не `BOND`)
|
||||
- **THEN** клиент отображает их компактными плитками в адаптивном grid с текстовой подписью доходности и без визуального логотипа
|
||||
@@ -0,0 +1,38 @@
|
||||
## 1. Backend Portfolio contract
|
||||
|
||||
- [x] 1.1 [backend] Добавить nullable `ticker` в нормализованную Portfolio Position и её OpenAPI DTO, заполняя его из уже разрешённой metadata с `null` для пустого или недоступного значения; расширить focused Portfolio service tests для доступного и отсутствующего ticker.
|
||||
- **Covers:** `portfolio-view` — `Нормализованный состав Portfolio`
|
||||
- **Verification:** `npm test -- portfolio.service.spec.ts --runInBand` в `backend` подтверждает ticker и null fallback в ответе Portfolio.
|
||||
- [x] 1.2 [backend] Опубликовать обновлённый runtime OpenAPI из backend и выполнить canonical backend quality gate.
|
||||
- **Covers:** `N/A` — runtime schema — технический источник frontend snapshot для утверждённого additive public contract.
|
||||
- **Verification:** `npm run verify` в `backend`.
|
||||
|
||||
## 2. Frontend generated contract и карточка акции
|
||||
|
||||
- [x] 2.1 [frontend] Скопировать опубликованный backend OpenAPI document в committed snapshot и регенерировать frontend types штатной командой без ручной правки generated файла.
|
||||
- **Covers:** `N/A` — generated contract синхронизирует утверждённое backend behavior с frontend consumer и не вводит самостоятельное observable behavior.
|
||||
- **Verification:** `npm run openapi:generate` в `frontend`; snapshot и generated `PortfolioPositionDto` содержат nullable `ticker`.
|
||||
- [x] 2.2 [frontend] Изменить presentation карточки так, чтобы `STOCK` показывал ticker с ISIN fallback, а `BOND` и другие типы сохраняли ISIN; добавить focused regression coverage этих трёх случаев.
|
||||
- **Covers:** `portfolio-view` — `Представление карточек позиций на клиенте`
|
||||
- **Verification:** focused frontend test card/account page подтверждает ticker-only stock, ISIN fallback и неизменённую облигацию.
|
||||
- [x] 2.3 [frontend] Выполнить canonical frontend gate после обновления generated contract и presentation.
|
||||
- **Covers:** `N/A` — canonical gate проверяет type safety, UI tests, boundaries и production build изменённого frontend.
|
||||
- **Verification:** `npm run verify` в `frontend`.
|
||||
|
||||
## 3. As-built documentation и canonical specs
|
||||
|
||||
- [x] 3.1 [docs] После проверенной реализации обновить Portfolio feature documentation: nullable ticker в Position и правило его отображения только на карточке акции с ISIN fallback.
|
||||
- **Covers:** `N/A` — as-built documentation фиксирует delivered behavior, но не является source of normative requirement.
|
||||
- **Verification:** `npm run verify` в `docs` и scoped review, что документация не описывает планировочные артефакты.
|
||||
- [x] 3.2 [specs] Подтвердить, что approved Proposal, Delta Specs и Design остаются единственным canonical описанием change до archive.
|
||||
- **Covers:** `N/A` — change artifacts уже владеют спецификацией behavior; задача не добавляет отдельного runtime behavior.
|
||||
- **Verification:** `openspec validate show-stock-tickers-on-position-cards --type change --strict --store wealth-ledger-specs` в `specs`.
|
||||
|
||||
## 4. Cross-area contract и completion review
|
||||
|
||||
- [x] 4.1 [workspace] Проверить обязательное совпадение backend runtime OpenAPI и committed frontend snapshot после их синхронизации.
|
||||
- **Covers:** `N/A` — cross-repository contract gate подтверждает delivery Decision, не создавая дополнительное observable behavior.
|
||||
- **Verification:** `npm run test:openapi-contract` в `backend`.
|
||||
- [x] 4.2 [workspace] Выполнить semantic verification против Proposal, Delta Specs и Design, затем independent read-only review backend mapping, generated contract, stock-card fallback и as-built docs.
|
||||
- **Covers:** `N/A` — cross-area verification подтверждает реализацию утверждённого behavior и rollback assumptions.
|
||||
- **Verification:** review фиксирует проверенные области и не содержит Critical или Important findings перед archive.
|
||||
Reference in New Issue
Block a user