spec: require RUB-only behavior (WealthLedger/main#55) #86

Merged
codex-bot merged 2 commits from refactor/55-rub-only into develop 2026-11-11 09:20:25 +03:00
12 changed files with 129 additions and 327 deletions
@@ -33,7 +33,7 @@ Issue: WealthLedger/main#54
| Индекс: отставание | Счёт со стоимостью ниже теневой: отрицательные `percentagePoints` и `money`, в UI «Отставание от рынка» | | Индекс: отставание | Счёт со стоимостью ниже теневой: отрицательные `percentagePoints` и `money`, в UI «Отставание от рынка» |
| Справочный индекс недоступен | IMOEX нет, MCFTRR есть: `reference.unavailableReason` задан, `benchmark` и `outperformance` есть, в UI нет пунктирной линии | | Справочный индекс недоступен | IMOEX нет, MCFTRR есть: `reference.unavailableReason` задан, `benchmark` и `outperformance` есть, в UI нет пунктирной линии |
| Недоступно: неизвестная дата открытия | Счёт с `historyBoundary = unknown`: `data: null`, `HISTORY_START_UNKNOWN`; в UI «Доходность недоступна», нет нулей | | Недоступно: неизвестная дата открытия | Счёт с `historyBoundary = unknown`: `data: null`, `HISTORY_START_UNKNOWN`; в UI «Доходность недоступна», нет нулей |
| Недоступно: USD в `all` | 3 счёта, один с USD: `completeness = incomplete`, `excludedAccounts` содержит счёт и `NON_RUB_ACTIVITY`; в UI предупреждение | | USD-поток игнорируется | Счёт с пополнением в USD: результат считается по рублёвым потокам, `excludedAccounts` пуст |
| Недоступно: даты оценок в `all` различаются | Последние оценки A и B в разные московские дни → `data: null`, `ACCOUNT_VALUATION_DATES_DIFFER`; стоимость не складывается | | Недоступно: даты оценок в `all` различаются | Последние оценки A и B в разные московские дни → `data: null`, `ACCOUNT_VALUATION_DATES_DIFFER`; стоимость не складывается |
| Недоступно: индекс недоступен | MOEX заблокирован и истории нет: `profit` есть, `benchmark.unavailableReason = INDEX_HISTORY_UNAVAILABLE`, `series = []` | | Недоступно: индекс недоступен | MOEX заблокирован и истории нет: `profit` есть, `benchmark.unavailableReason = INDEX_HISTORY_UNAVAILABLE`, `series = []` |
| Недоступно: остальные причины | Перевод бумаг без пары → `UNVALUED_SECURITIES_TRANSFER`; позиция без цены → `PORTFOLIO_VALUATION_INCOMPLETE`; незавершённая синхронизация → `OPERATIONS_HISTORY_INCOMPLETE` | | Недоступно: остальные причины | Перевод бумаг без пары → `UNVALUED_SECURITIES_TRANSFER`; позиция без цены → `PORTFOLIO_VALUATION_INCOMPLETE`; незавершённая синхронизация → `OPERATIONS_HISTORY_INCOMPLETE` |
@@ -15,7 +15,7 @@
- **THEN** система не раскрывает никаких значений результата этого счёта - **THEN** система не раскрывает никаких значений результата этого счёта
### Requirement: Внешние денежные потоки ### Requirement: Внешние денежные потоки
Внешним денежным потоком MUST считаться только пополнение счёта рублями (положительный поток) и вывод рублей со счёта (отрицательный поток) на дату исполнения. Сделки, купоны, дивиденды, погашения, амортизация, налоги и комиссии MUST NOT считаться внешними потоками: их влияние входит в текущую стоимость. В режиме `all` переводы денег и бумаг между счетами, входящими в результат, MUST NOT считаться внешними потоками. Чистые внешние вложения MUST равняться точной сумме внешних потоков периода. Прибыль MUST равняться точной разности текущей стоимости и чистых внешних вложений. Внешним денежным потоком MUST считаться только пополнение счёта рублями (положительный поток) и вывод рублей со счёта (отрицательный поток) на дату исполнения. Пополнения и выводы не в RUB MUST NOT учитываться и MUST NOT делать результат недоступным: система поддерживает только RUB и не конвертирует валюты. Сделки, купоны, дивиденды, погашения, амортизация, налоги и комиссии MUST NOT считаться внешними потоками: их влияние входит в текущую стоимость. В режиме `all` переводы денег и бумаг между счетами, входящими в результат, MUST NOT считаться внешними потоками. Чистые внешние вложения MUST равняться точной сумме внешних потоков периода. Прибыль MUST равняться точной разности текущей стоимости и чистых внешних вложений.
#### Scenario: Пополнения, вывод и дивиденды #### Scenario: Пополнения, вывод и дивиденды
- **WHEN** на счёт внесено 100 000 RUB и 50 000 RUB, выведено 20 000 RUB, получены дивиденды 3 000 RUB, а текущая стоимость Portfolio 140 000 RUB - **WHEN** на счёт внесено 100 000 RUB и 50 000 RUB, выведено 20 000 RUB, получены дивиденды 3 000 RUB, а текущая стоимость Portfolio 140 000 RUB
@@ -81,7 +81,6 @@
### Requirement: Недоступный и неполный результат ### Requirement: Недоступный и неполный результат
Система MUST NOT возвращать числовой результат, выдаваемый за доходность, если его нельзя рассчитать честно. Для одного счёта результат MUST быть недоступным с машиночитаемой причиной, если: Система MUST NOT возвращать числовой результат, выдаваемый за доходность, если его нельзя рассчитать честно. Для одного счёта результат MUST быть недоступным с машиночитаемой причиной, если:
- начало истории операций неизвестно; - начало истории операций неизвестно;
- есть поток или позиция не в RUB;
- есть ввод или вывод ценных бумаг без денежной оценки, а в режиме `all` — неподтверждённый кандидат на перевод между своими счетами; - есть ввод или вывод ценных бумаг без денежной оценки, а в режиме `all` — неподтверждённый кандидат на перевод между своими счетами;
- текущая оценка Portfolio неполная; - текущая оценка Portfolio неполная;
- история операций не получена полностью. - история операций не получена полностью.
@@ -92,9 +91,9 @@
- **WHEN** у счёта неизвестно начало истории операций - **WHEN** у счёта неизвестно начало истории операций
- **THEN** результат счёта недоступен с причиной «начало истории неизвестно», и ни одно значение не показывается как ноль - **THEN** результат счёта недоступен с причиной «начало истории неизвестно», и ни одно значение не показывается как ноль
#### Scenario: Счёт с операциями в USD в режиме «Все счета» #### Scenario: Пополнение в USD не учитывается
- **WHEN** в режиме `all` один из трёх счетов содержит пополнение в USD - **WHEN** у рублёвого счёта есть пополнение в USD
- **THEN** результат рассчитывается по двум оставшимся счетам и помечается как неполный с указанием исключённого счёта и причины «операции не в рублях» - **THEN** результат рассчитывается только по рублёвым пополнениям и выводам, без причины недоступности
#### Scenario: Счета оценены в разные московские дни #### Scenario: Счета оценены в разные московские дни
- **WHEN** в режиме `all` последние полные оценки включённых счетов относятся к разным московским календарным дням - **WHEN** в режиме `all` последние полные оценки включённых счетов относятся к разным московским календарным дням
@@ -84,9 +84,7 @@ Issue: WealthLedger/main#54
| `INPUT_SECURITIES`, `OUTPUT_SECURITIES`, `TRANS_IIS_BS`, `TRANS_BS_BS` | Перевод бумаг: у отдельного счёта всегда `UNVALUED_SECURITIES_TRANSFER`; в `all` — см. ниже | | `INPUT_SECURITIES`, `OUTPUT_SECURITIES`, `TRANS_IIS_BS`, `TRANS_BS_BS` | Перевод бумаг: у отдельного счёта всегда `UNVALUED_SECURITIES_TRANSFER`; в `all` — см. ниже |
| Сделки, купоны, дивиденды, погашения, амортизация, налоги, комиссии (включая `OUT_FEE`, `OUTPUT_PENALTY`), прочие | Внутренние, в потоки не входят | | Сделки, купоны, дивиденды, погашения, амортизация, налоги, комиссии (включая `OUT_FEE`, `OUTPUT_PENALTY`), прочие | Внутренние, в потоки не входят |
- Поток в валюте, отличной от RUB, даёт счёту `NON_RUB_ACTIVITY`. - Поток в валюте, отличной от RUB, не учитывается (RUB-only, WealthLedger/main#55).
- `totalsByCurrency` последнего Portfolio с ненулевым значением в другой
валюте тоже даёт `NON_RUB_ACTIVITY`.
- Учитываются только операции с `occurredAt ≤ fetchedAt` Portfolio этого - Учитываются только операции с `occurredAt ≤ fetchedAt` Portfolio этого
счёта. Иначе пополнение после оценки исказит результат. счёта. Иначе пополнение после оценки исказит результат.
- Дата потока — календарный день `occurredAt` в Europe/Moscow. - Дата потока — календарный день `occurredAt` в Europe/Moscow.
@@ -119,9 +117,8 @@ T-Bank не передаёт в операции счёт-контрагента
2. `OPERATIONS_HISTORY_INCOMPLETE`: покрытие не `complete`. 2. `OPERATIONS_HISTORY_INCOMPLETE`: покрытие не `complete`.
3. `PORTFOLIO_VALUATION_INCOMPLETE`: нет Portfolio или у ненулевой позиции 3. `PORTFOLIO_VALUATION_INCOMPLETE`: нет Portfolio или у ненулевой позиции
нет вычислимой стоимости (то же правило, что у `portfolio-value-history`). нет вычислимой стоимости (то же правило, что у `portfolio-value-history`).
4. `NON_RUB_ACTIVITY`. 4. `UNVALUED_SECURITIES_TRANSFER`.
5. `UNVALUED_SECURITIES_TRANSFER`. 5. `UNCONFIRMED_TRANSFER` (только `all`, когда есть кандидат на внутренний
6. `UNCONFIRMED_TRANSFER` (только `all`, когда есть кандидат на внутренний
перевод без доказанной связи). перевод без доказанной связи).
Как причины попадают в ответ: Как причины попадают в ответ:
@@ -58,7 +58,6 @@ components:
enum: enum:
- HISTORY_START_UNKNOWN - HISTORY_START_UNKNOWN
- OPERATIONS_HISTORY_INCOMPLETE - OPERATIONS_HISTORY_INCOMPLETE
- NON_RUB_ACTIVITY
- UNVALUED_SECURITIES_TRANSFER - UNVALUED_SECURITIES_TRANSFER
- UNCONFIRMED_TRANSFER - UNCONFIRMED_TRANSFER
- ACCOUNT_VALUATION_DATES_DIFFER - ACCOUNT_VALUATION_DATES_DIFFER
@@ -1,7 +1,7 @@
# brokerage-account-income-analytics Specification # brokerage-account-income-analytics Specification
## Purpose ## Purpose
Позволяет User анализировать фактически полученные купоны и дивиденды одного Brokerage Account по исходным валютам, проверять состав результата и отдельно видеть известный ближайший прогноз. Позволяет User анализировать фактически полученные купоны и дивиденды одного Brokerage Account в RUB, проверять состав результата и отдельно видеть известный ближайший прогноз.
## Requirements ## Requirements
@@ -41,12 +41,12 @@
### Requirement: Realized income uses exact executed coupon and dividend payments ### Requirement: Realized income uses exact executed coupon and dividend payments
Система MUST рассчитывать фактически полученный доход только по исполненным Ledger Entries с точным upstream-типом coupon или dividend. Для каждой исходной валюты система MUST отдельно возвращать общий итог, купонный subtotal и дивидендный subtotal как точные суммы исходных broker-reported payments. Знак каждой выплаты MUST сохраняться, поэтому отрицательная корректировка MUST уменьшать соответствующий итог и оставаться доступной в составе результата. Dividend transfer, погашение principal, offer, trade, transfer, fee, tax, overnight income, иной тип операции, нереализованный результат и изменение рыночной цены MUST NOT входить в фактически полученный доход. Система MUST NOT выполнять валютную конвертацию или формировать общий итог по разным валютам. Система MUST рассчитывать фактически полученный доход только по исполненным Ledger Entries с точным upstream-типом coupon или dividend. Система MUST возвращать общий итог, купонный subtotal и дивидендный subtotal как точные суммы broker-reported payments в RUB. Знак каждой выплаты MUST сохраняться, поэтому отрицательная корректировка MUST уменьшать соответствующий итог и оставаться доступной в составе результата. Dividend transfer, погашение principal, offer, trade, transfer, fee, tax, overnight income, иной тип операции, нереализованный результат и изменение рыночной цены MUST NOT входить в фактически полученный доход. Выплаты в валютах, отличных от RUB, MUST отфильтровываться при получении и MUST NOT входить в доход, subtotal или итог.
#### Scenario: Купоны и дивиденды образуют раздельные subtotals #### Scenario: Купоны и дивиденды образуют раздельные subtotals
- **WHEN** выбранный период содержит исполненные купонные и дивидендные выплаты в одной валюте - **WHEN** выбранный период содержит исполненные купонные и дивидендные выплаты в RUB
- **THEN** система возвращает их точные раздельные subtotals и общий итог, равный их сумме в этой валюте - **THEN** система возвращает их точные раздельные subtotals и общий итог, равный их сумме
#### Scenario: Отрицательная корректировка сохраняет знак #### Scenario: Отрицательная корректировка сохраняет знак
@@ -58,11 +58,6 @@
- **WHEN** выбранный период наряду с coupon и dividend содержит tax, fee, trade, transfer, maturity или иной тип Ledger Entry - **WHEN** выбранный период наряду с coupon и dividend содержит tax, fee, trade, transfer, maturity или иной тип Ledger Entry
- **THEN** система не включает эти записи в coupon subtotal, dividend subtotal или total - **THEN** система не включает эти записи в coupon subtotal, dividend subtotal или total
#### Scenario: Несколько валют остаются разделёнными
- **WHEN** выбранный период содержит подходящие выплаты в нескольких исходных валютах
- **THEN** система возвращает отдельные итоги по каждой валюте без сконвертированного grand total
### Requirement: User explicitly applies an analytics period represented in the URL ### Requirement: User explicitly applies an analytics period represented in the URL
Страница выбранного Brokerage Account «Пассивный доход» MUST позволять User редактировать черновик `from` и `to`, не изменяя видимую аналитику до явного применения. Верхний блок фильтров MUST предоставлять два подписанных календарных поля начала и окончания периода с тем же взаимодействием выбора даты, что в истории событий портфеля, и отображением дат в формате `ДД.ММ.ГГГГ`. Поля и действия применения и сброса MUST быть доступны с клавиатуры. Явное применение MUST заменить применённый период, загрузить соответствующий результат и представить применённые даты в URL страницы. Недопустимый черновик MUST показывать ошибку соответствующего поля и MUST NOT заменять последний применённый период. Сброс MUST немедленно вернуть применённый период к текущему месяцу по сегодняшний день и синхронизировать URL. Перезагрузка или открытие URL с допустимым периодом MUST восстановить тот же применённый период. Страница MUST NOT дублировать период отдельной подписью под блоком фильтров. Действие перехода на страницу MUST быть доступно из выбранного Brokerage Account, а не из глобальной навигации. Страница выбранного Brokerage Account «Пассивный доход» MUST позволять User редактировать черновик `from` и `to`, не изменяя видимую аналитику до явного применения. Верхний блок фильтров MUST предоставлять два подписанных календарных поля начала и окончания периода с тем же взаимодействием выбора даты, что в истории событий портфеля, и отображением дат в формате `ДД.ММ.ГГГГ`. Поля и действия применения и сброса MUST быть доступны с клавиатуры. Явное применение MUST заменить применённый период, загрузить соответствующий результат и представить применённые даты в URL страницы. Недопустимый черновик MUST показывать ошибку соответствующего поля и MUST NOT заменять последний применённый период. Сброс MUST немедленно вернуть применённый период к текущему месяцу по сегодняшний день и синхронизировать URL. Перезагрузка или открытие URL с допустимым периодом MUST восстановить тот же применённый период. Страница MUST NOT дублировать период отдельной подписью под блоком фильтров. Действие перехода на страницу MUST быть доступно из выбранного Brokerage Account, а не из глобальной навигации.
@@ -141,7 +136,7 @@ breakdown как полный результат. При временной не
### Requirement: Account overview has an independent current-month income summary ### Requirement: Account overview has an independent current-month income summary
Обзор выбранного Brokerage Account MUST показывать компактный source-currency Обзор выбранного Brokerage Account MUST показывать компактную
summary «Получено купонами и дивидендами» за период от первого дня текущего summary «Получено купонами и дивидендами» за период от первого дня текущего
месяца по Europe/Moscow до сегодняшнего дня включительно и действие перехода месяца по Europe/Moscow до сегодняшнего дня включительно и действие перехода
на страницу «Пассивный доход». Summary MUST всегда использовать этот current-month на страницу «Пассивный доход». Summary MUST всегда использовать этот current-month
@@ -154,7 +149,7 @@ MUST NOT заменять успешно отображённый Portfolio ил
#### Scenario: Обзор показывает доход текущего месяца #### Scenario: Обзор показывает доход текущего месяца
- **WHEN** обзор Brokerage Account загружен и current-month analytics успешно рассчитана - **WHEN** обзор Brokerage Account загружен и current-month analytics успешно рассчитана
- **THEN** он показывает отдельные source-currency итоги с начала текущего месяца по сегодняшний день и переход к полной аналитике - **THEN** он показывает итог в RUB с начала текущего месяца по сегодняшний день и переход к полной аналитике
#### Scenario: Пользовательский период не влияет на обзор #### Scenario: Пользовательский период не влияет на обзор
- **WHEN** User ранее применил на странице аналитики период, отличный от текущего месяца - **WHEN** User ранее применил на странице аналитики период, отличный от текущего месяца
@@ -166,7 +161,7 @@ MUST NOT заменять успешно отображённый Portfolio ил
### Requirement: Income payments remain traceable to exact Ledger Entries ### Requirement: Income payments remain traceable to exact Ledger Entries
В разделе «Выплаты» страница MUST показывать единый список карточек точных исполненных Ledger Entries применённого периода в исходном серверном порядке entries. Каждая выплата MUST иметь собственную сразу видимую карточку с описанием, датой исполнения, доступной identity Instrument и signed Money в исходной валюте. Купоны и дивиденды MUST использовать то же представление выплаты, что и на экранах журнала, прогноза и графиков инструментов, при этом карточка MUST обозначать эту сумму именно как фактически исполненную, а не прогнозную. В режиме одного Brokerage Account страница MUST NOT добавлять внешние группировочные карточки или заголовки по Instrument либо валюте, subtotals инструмента или количество выплат инструмента. В режиме «Все счета» записи раздела «Выплаты» MUST группироваться по тем же правилам ключа, состава, порядка и итога, что и записи журнала по Requirements «All-accounts ledger groups same-day entries of one instrument», «All-accounts ledger group shows each entry and an exact total» и «All-accounts ledger grouping preserves order and limits» capability `brokerage-account-ledger`. Каждая запись при этом MUST оставаться отдельной сразу видимой карточкой внутри группы, а итог группы MUST NOT показываться, пока полнота периода не подтверждена; заголовки валютных групп и receipt count MUST NOT добавляться. Список MUST сохранять отрицательные корректировки и повторяющиеся operation IDs как отдельные записи и MUST NOT включать записи вне аналитического итога. Backend MUST обеспечивать согласованность entries, instrumentKey, валюты и receipt count при формировании ответа из одного набора записей. Frontend SHALL доверять типизированному contract без второго runtime consistency validator и MUST NOT пересчитывать аналитические денежные итоги. Единственное исключение — итог визуальной группы режима «Все счета»: это точная сумма показанных записей группы по каждой валюте, и он MUST NOT подменять аналитические итоги. В разделе «Выплаты» страница MUST показывать единый список карточек точных исполненных Ledger Entries применённого периода в исходном серверном порядке entries. Каждая выплата MUST иметь собственную сразу видимую карточку с описанием, датой исполнения, доступной identity Instrument и signed Money в RUB. Купоны и дивиденды MUST использовать то же представление выплаты, что и на экранах журнала, прогноза и графиков инструментов, при этом карточка MUST обозначать эту сумму именно как фактически исполненную, а не прогнозную. В режиме одного Brokerage Account страница MUST NOT добавлять внешние группировочные карточки или заголовки по Instrument, subtotals инструмента или количество выплат инструмента. В режиме «Все счета» записи раздела «Выплаты» MUST группироваться по тем же правилам ключа, состава, порядка и итога, что и записи журнала по Requirements «All-accounts ledger groups same-day entries of one instrument», «All-accounts ledger group shows each entry and an exact total» и «All-accounts ledger grouping preserves order and limits» capability `brokerage-account-ledger`. Каждая запись при этом MUST оставаться отдельной сразу видимой карточкой внутри группы, а итог группы MUST NOT показываться, пока полнота периода не подтверждена; receipt count MUST NOT добавляться. Список MUST сохранять отрицательные корректировки и повторяющиеся operation IDs как отдельные записи и MUST NOT включать записи вне аналитического итога. Backend MUST обеспечивать согласованность entries, instrumentKey и receipt count при формировании ответа из одного набора записей. Frontend SHALL доверять типизированному contract без второго runtime consistency validator и MUST NOT пересчитывать аналитические денежные итоги. Единственное исключение — итог визуальной группы режима «Все счета»: это точная сумма показанных записей группы в RUB, и он MUST NOT подменять аналитические итоги.
#### Scenario: Каждая выплата показана отдельной карточкой #### Scenario: Каждая выплата показана отдельной карточкой
@@ -183,29 +178,19 @@ MUST NOT заменять успешно отображённый Portfolio ил
- **WHEN** доход содержит отрицательную корректировку либо несколько включённых entries с одинаковым operation ID - **WHEN** доход содержит отрицательную корректировку либо несколько включённых entries с одинаковым operation ID
- **THEN** каждая запись остаётся отдельной карточкой, а корректировка отображается с исходным знаком - **THEN** каждая запись остаётся отдельной карточкой, а корректировка отображается с исходным знаком
#### Scenario: Валюта видна в сумме каждой выплаты
- **WHEN** подходящие в режиме одного Brokerage Account выплаты одного Instrument представлены в нескольких исходных валютах
- **THEN** каждая карточка показывает свою Money с исходной валютой, без отдельного заголовка валютной группы или объединённого мультивалютного итога
#### Scenario: Выплаты одной бумаги на двух счетах в режиме «Все счета» #### Scenario: Выплаты одной бумаги на двух счетах в режиме «Все счета»
- **WHEN** в режиме «Все счета» за один день получены купон по облигации на брокерском счёте +200 RUB и купон по той же облигации на ИИС +100 RUB - **WHEN** в режиме «Все счета» за один день получены купон по облигации на брокерском счёте +200 RUB и купон по той же облигации на ИИС +100 RUB
- **THEN** раздел «Выплаты» показывает их одной группой с итогом +300 RUB, внутри которой видны отдельные карточки каждого счёта, а аналитические итоги страницы не меняются - **THEN** раздел «Выплаты» показывает их одной группой с итогом +300 RUB, внутри которой видны отдельные карточки каждого счёта, а аналитические итоги страницы не меняются
### Requirement: Analytics summary remains readable per currency ### Requirement: Analytics summary remains readable
Страница MUST обозначать фактический результат как «Получено купонами и дивидендами» и для каждой исходной валюты показывать total, coupon subtotal и dividend subtotal выбранного периода. Она MUST объяснять, что tax operations не включены, не называя фактический результат gross, net, суммой до налогов, суммой после налогов или налоговым расчётом. Значения MUST оставаться доступными как читаемый текст, не только как графика, цвет или положение. Страница MUST NOT показывать блок «Динамика», его график, переключатель валюты графика или текстовый список временных buckets независимо от длительности выбранного периода. Страница MUST NOT показывать блоки «Предыдущий период» и «Годом ранее» или сообщения о недоступности сравнений. Страница MUST обозначать фактический результат как «Получено купонами и дивидендами» и показывать total, coupon subtotal и dividend subtotal выбранного периода в RUB. Она MUST объяснять, что tax operations не включены, не называя фактический результат gross, net, суммой до налогов, суммой после налогов или налоговым расчётом. Значения MUST оставаться доступными как читаемый текст, не только как графика, цвет или положение. Страница MUST NOT показывать блок «Динамика», его график или текстовый список временных buckets независимо от длительности выбранного периода. Страница MUST NOT показывать блоки «Предыдущий период» и «Годом ранее» или сообщения о недоступности сравнений.
#### Scenario: Итоги дохода доступны как текст #### Scenario: Итоги дохода доступны как текст
- **WHEN** выбранный период содержит исполненные купонные или дивидендные выплаты - **WHEN** выбранный период содержит исполненные купонные или дивидендные выплаты
- **THEN** страница показывает читаемые total, coupon subtotal и dividend subtotal с обозначением фактически полученного дохода и объяснением исключения tax operations - **THEN** страница показывает читаемые total, coupon subtotal и dividend subtotal в RUB с обозначением фактически полученного дохода и объяснением исключения tax operations
#### Scenario: Итоги нескольких валют остаются разделёнными
- **WHEN** результат содержит подходящие выплаты в нескольких исходных валютах
- **THEN** страница показывает отдельные читаемые итоги каждой валюты без конвертации и общего итога разных валют
#### Scenario: Динамика отсутствует для периода любой длительности #### Scenario: Динамика отсутствует для периода любой длительности
@@ -43,11 +43,11 @@
- **THEN** система возвращает следующую страницу без повторения записей предыдущей страницы - **THEN** система возвращает следующую страницу без повторения записей предыдущей страницы
### Requirement: Ledger entries preserve broker operation meaning ### Requirement: Ledger entries preserve broker operation meaning
Система MUST представлять одну upstream-операцию одной записью журнала и MUST передавать её timestamp, тип операции, категорию, описание, доступные идентификаторы инструмента, доступную presentation metadata инструмента, денежную выплату, количество, цену, НКД и комиссию. Денежные поля MUST сохранять исходную валюту и точное десятичное значение. Система MUST представлять одну upstream-операцию одной записью журнала и MUST передавать её timestamp, тип операции, категорию, описание, доступные идентификаторы инструмента, доступную presentation metadata инструмента, денежную выплату, количество, цену, НКД и комиссию. Денежные поля MUST быть в RUB и сохранять точное десятичное значение. Операции в валютах, отличных от RUB, MUST отфильтровываться при получении и MUST NOT попадать в журнал.
#### Scenario: Отображение купонной выплаты #### Scenario: Отображение купонной выплаты
- **WHEN** T-Bank возвращает исполненную купонную операцию с выплатой и данными инструмента - **WHEN** T-Bank возвращает исполненную купонную операцию с выплатой и данными инструмента
- **THEN** запись журнала определяет её как income и передаёт исходную сумму выплаты, валюту и данные инструмента - **THEN** запись журнала определяет её как income и передаёт сумму выплаты в RUB и данные инструмента
#### Scenario: Отображение комиссии сделки без синтетической записи #### Scenario: Отображение комиссии сделки без синтетической записи
- **WHEN** T-Bank возвращает исполненную сделку, содержащую выплату и комиссию - **WHEN** T-Bank возвращает исполненную сделку, содержащую выплату и комиссию
@@ -88,7 +88,7 @@
### Requirement: Full ledger presents compact rows ### Requirement: Full ledger presents compact rows
Страница полного журнала MUST показывать для каждой записи дату, тип операции, инструмент и денежное изменение в исходной валюте в компактной структурированной строке. При доступной presentation metadata инструмента строка MUST показывать его визуальную идентичность; при её отсутствии MUST использовать понятный текстовый fallback. Для купонных и дивидендных операций на странице полного журнала и в личной истории инструмента MUST использоваться то же представление выплаты, что в доходе, прогнозе и графиках инструментов, с обозначением исполненного движения. Для каждой записи денежное изменение в исходной валюте MUST сопровождаться подписью конкретного типа события и знаком: для купонной операции — «Купон», для дивидендной — «Дивиденд», для остальных записей — подпись их категории («Сделка», «Налог», «Комиссия», «Перевод», «Прочее»). Строка MUST NOT повторять подпись типа события во вторичной метадате записи. Знак «+» / «−» и семантический цвет MUST оставаться носителями направления и MUST NOT заменяться только текстом. Страница MUST обозначать записи без движения денег нейтрально и MUST NOT показывать цену или количество. В группе режима «Все счета» дата, тип операции, инструмент и его визуальная идентичность MUST показываться один раз в заголовке группы, а строка каждой записи группы MUST показывать её счёт и денежное изменение с подписью типа и знаком. Страница полного журнала MUST показывать для каждой записи дату, тип операции, инструмент и денежное изменение в RUB в компактной структурированной строке. При доступной presentation metadata инструмента строка MUST показывать его визуальную идентичность; при её отсутствии MUST использовать понятный текстовый fallback. Для купонных и дивидендных операций на странице полного журнала и в личной истории инструмента MUST использоваться то же представление выплаты, что в доходе, прогнозе и графиках инструментов, с обозначением исполненного движения. Для каждой записи денежное изменение в RUB MUST сопровождаться подписью конкретного типа события и знаком: для купонной операции — «Купон», для дивидендной — «Дивиденд», для остальных записей — подпись их категории («Сделка», «Налог», «Комиссия», «Перевод», «Прочее»). Строка MUST NOT повторять подпись типа события во вторичной метадате записи. Знак «+» / «−» и семантический цвет MUST оставаться носителями направления и MUST NOT заменяться только текстом. Страница MUST обозначать записи без движения денег нейтрально и MUST NOT показывать цену или количество. В группе режима «Все счета» дата, тип операции, инструмент и его визуальная идентичность MUST показываться один раз в заголовке группы, а строка каждой записи группы MUST показывать её счёт и денежное изменение с подписью типа и знаком.
#### Scenario: Отображение записи только с инструментом #### Scenario: Отображение записи только с инструментом
@@ -98,12 +98,12 @@
#### Scenario: Отображение поступления #### Scenario: Отображение поступления
- **WHEN** полный журнал отображает запись с положительной денежной выплатой - **WHEN** полный журнал отображает запись с положительной денежной выплатой
- **THEN** строка явно показывает поступление знаком и подписью конкретного типа события («Купон», «Дивиденд» или категории) и его денежное изменение в исходной валюте, не полагаясь только на цвет - **THEN** строка явно показывает поступление знаком и подписью конкретного типа события («Купон», «Дивиденд» или категории) и его денежное изменение в RUB, не полагаясь только на цвет
#### Scenario: Отображение списания #### Scenario: Отображение списания
- **WHEN** полный журнал отображает запись с отрицательной денежной выплатой - **WHEN** полный журнал отображает запись с отрицательной денежной выплатой
- **THEN** строка явно показывает списание знаком и подписью конкретного типа события («Купон», «Дивиденд» или категории) и его денежное изменение в исходной валюте, не полагаясь только на цвет - **THEN** строка явно показывает списание знаком и подписью конкретного типа события («Купон», «Дивиденд» или категории) и его денежное изменение в RUB, не полагаясь только на цвет
#### Scenario: Отображение купона #### Scenario: Отображение купона
@@ -231,11 +231,7 @@
- **THEN** записи показаны отдельными строками без группы, как до изменения - **THEN** записи показаны отдельными строками без группы, как до изменения
### Requirement: All-accounts ledger group shows each entry and an exact total ### Requirement: All-accounts ledger group shows each entry and an exact total
Заголовок группы журнала MUST показывать дату, Instrument с визуальной идентичностью, подпись типа операции, как в строке журнала, и итог. Под ним MUST всегда быть видна, без раскрытия, отдельная строка каждой записи со счётом и денежным изменением. Итог SHALL быть точной суммой со знаком по каждой исходной валюте без конвертации и округления. Итог MUST NOT показываться, если у записи группы нет денежного изменения (включая нулевое) или полнота периода не подтверждена. Заголовок группы журнала MUST показывать дату, Instrument с визуальной идентичностью, подпись типа операции, как в строке журнала, и итог. Под ним MUST всегда быть видна, без раскрытия, отдельная строка каждой записи со счётом и денежным изменением. Итог SHALL быть точной суммой со знаком в RUB без округления. Итог MUST NOT показываться, если у записи группы нет денежного изменения (включая нулевое) или полнота периода не подтверждена.
#### Scenario: Итог по нескольким валютам
- **WHEN** группа содержит записи с денежными изменениями в RUB и в USD
- **THEN** заголовок показывает отдельный точный итог в RUB и отдельный в USD, без конвертации
#### Scenario: Итог со знаком #### Scenario: Итог со знаком
- **WHEN** группа содержит поступление +150.10 RUB и отрицательную корректировку −50.05 RUB - **WHEN** группа содержит поступление +150.10 RUB и отрицательную корректировку −50.05 RUB
@@ -42,7 +42,7 @@ Instrument в Future Cash Event MUST иметь тот же доменный с
#### Scenario: Прогноз купона по текущей позиции #### Scenario: Прогноз купона по текущей позиции
- **WHEN** поддерживаемая облигация в Portfolio Snapshot на `calculatedAt` имеет известный купон в пределах исходного окна прогноза - **WHEN** поддерживаемая облигация в Portfolio Snapshot на `calculatedAt` имеет известный купон в пределах исходного окна прогноза
- **THEN** система возвращает сохранённое событие coupon с прогнозной суммой до налогов как Money в исходной валюте, рассчитанной по количеству облигаций этого Snapshot - **THEN** система возвращает сохранённое событие coupon с прогнозной суммой до налогов как Money в RUB, рассчитанной по количеству облигаций этого Snapshot
#### Scenario: Прогноз дивиденда #### Scenario: Прогноз дивиденда
- **WHEN** поддерживаемая акция в Portfolio Snapshot на `calculatedAt` имеет объявленный дивиденд с датой фиксации реестра в пределах исходного окна прогноза - **WHEN** поддерживаемая акция в Portfolio Snapshot на `calculatedAt` имеет объявленный дивиденд с датой фиксации реестра в пределах исходного окна прогноза
@@ -92,7 +92,7 @@ Instrument в Future Cash Event MUST иметь тот же доменный с
Страница Brokerage Account MUST предоставлять действие перехода на отдельную Страница Brokerage Account MUST предоставлять действие перехода на отдельную
страницу «Будущие события». Эта страница MUST показывать все полученные события страницу «Будущие события». Эта страница MUST показывать все полученные события
компактным списком за выбранный период без пагинации и суммарного итога списка, с датой, типом, компактным списком за выбранный период без пагинации и суммарного итога списка, с датой, типом,
инструментом и прогнозной суммой в исходной валюте, если она известна. Прогнозный итог инструментом и прогнозной суммой в RUB, если она известна. Прогнозный итог
визуальной группы режима «Все счета» не является суммарным итогом списка. Страница визуальной группы режима «Все счета» не является суммарным итогом списка. Страница
MUST объяснять, что прогноз основан на позиции счёта, указан до налогов и может MUST объяснять, что прогноз основан на позиции счёта, указан до налогов и может
быть неполным из-за доступности данных источника. Это предметное ограничение быть неполным из-за доступности данных источника. Это предметное ограничение
@@ -222,7 +222,7 @@ continue to use the existing URL-backed period behavior.
- **THEN** события показаны отдельными строками без группы, как до изменения - **THEN** события показаны отдельными строками без группы, как до изменения
### Requirement: All-accounts upcoming event group shows each event and a forecast total ### Requirement: All-accounts upcoming event group shows each event and a forecast total
Заголовок группы событий MUST показывать дату (для дивиденда — как дату реестра, если она такая), Instrument, тип и прогнозный итог. Под ним MUST всегда быть видна, без раскрытия, отдельная строка каждого события со счётом и прогнозной суммой (или пометкой о неизвестной сумме). Итог SHALL быть точной суммой со знаком по каждой валюте без конвертации и округления. Итог MUST NOT показываться, если у события группы нет суммы или полнота прогноза не подтверждена. Заголовок группы событий MUST показывать дату (для дивиденда — как дату реестра, если она такая), Instrument, тип и прогнозный итог. Под ним MUST всегда быть видна, без раскрытия, отдельная строка каждого события со счётом и прогнозной суммой (или пометкой о неизвестной сумме). Итог SHALL быть точной суммой со знаком в RUB без округления. Итог MUST NOT показываться, если у события группы нет суммы или полнота прогноза не подтверждена.
#### Scenario: Событие с неизвестной суммой в группе #### Scenario: Событие с неизвестной суммой в группе
- **WHEN** в режиме «Все счета» группа содержит дивиденд на одном счёте с известной суммой и дивиденд по той же бумаге на ту же дату на другом счёте без суммы - **WHEN** в режиме «Все счета» группа содержит дивиденд на одном счёте с известной суммой и дивиденд по той же бумаге на ту же дату на другом счёте без суммы
@@ -232,10 +232,6 @@ continue to use the existing URL-backed period behavior.
- **WHEN** в режиме «Все счета» на одну дату ожидается оферта по одной облигации на двух счетах - **WHEN** в режиме «Все счета» на одну дату ожидается оферта по одной облигации на двух счетах
- **THEN** они показаны одной группой без итога, и каждая строка показывает свой счёт - **THEN** они показаны одной группой без итога, и каждая строка показывает свой счёт
#### Scenario: Прогнозный итог по нескольким валютам
- **WHEN** группа содержит дивиденд 10 USD на одном счёте и дивиденд 200 RUB по той же бумаге на ту же дату на другом счёте
- **THEN** заголовок показывает отдельные прогнозные итоги 10 USD и 200 RUB без конвертации
#### Scenario: Прогнозный итог скрыт при неполном прогнозе #### Scenario: Прогнозный итог скрыт при неполном прогнозе
- **WHEN** в режиме «Все счета» прогноз обозначен неполным, и на странице есть группа событий с известными суммами - **WHEN** в режиме «Все счета» прогноз обозначен неполным, и на странице есть группа событий с известными суммами
- **THEN** заголовок группы не показывает итог, а строки событий остаются видны со своими суммами - **THEN** заголовок группы не показывает итог, а строки событий остаются видны со своими суммами
@@ -9,7 +9,7 @@
### Requirement: Список активных брокерских счетов ### Requirement: Список активных брокерских счетов
Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts`, возвращающий `{ data, meta }`, где `data` является сохранённым подтверждённым массивом открытых поддерживаемых инвестиционных счетов T-Bank текущего подключения User в ранее подтверждённом upstream-порядке, а `meta` соответствует наблюдаемому состоянию resource по `tbank-data-sync`. Обычный GET MUST NOT обращаться к 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 удалить внешние пробелы из имени и при пустом результате использовать непустое название соответствующего типа. Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts`, возвращающий `{ data, meta }`, где `data` является сохранённым подтверждённым массивом открытых поддерживаемых инвестиционных счетов T-Bank текущего подключения User в ранее подтверждённом upstream-порядке, а `meta` соответствует наблюдаемому состоянию resource по `tbank-data-sync`. Обычный GET MUST NOT обращаться к 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`. Каждый элемент MUST также содержать nullable `portfolioSummary`. Доступная сводка MUST содержать точный Money-итог последнего Portfolio в RUB, включающий стоимость позиций и Cash Balance. Cash Balance MUST NOT выдаваться за самостоятельный второй итог. Если последний Portfolio конкретного доступного счёта отсутствует либо временно недоступен, `portfolioSummary` MUST быть `null`, не исключая этот счёт из списка и не делая список неуспешным. Пустой доступный Portfolio MUST передаваться доступной сводкой с нулевым итогом отсутствия позиций и денежных остатков либо без итоговой суммы; он MUST NOT передаваться как `null`.
Список Brokerage Account и его сводки являются результатом наблюдения текущего брокерского подключения и MUST NOT создавать ожидание, что WealthLedger хранит долговременное право владения счётом или Portfolio независимо от T-Bank connection. Список Brokerage Account и его сводки являются результатом наблюдения текущего брокерского подключения и MUST NOT создавать ожидание, что WealthLedger хранит долговременное право владения счётом или Portfolio независимо от T-Bank connection.
@@ -29,17 +29,13 @@
- **WHEN** последний подтверждённый счёт не имеет даты открытия - **WHEN** последний подтверждённый счёт не имеет даты открытия
- **THEN** система отвечает этим счётом с `openedOn: null` - **THEN** система отвечает этим счётом с `openedOn: null`
#### Scenario: Мультивалютная сводка счёта
- **WHEN** последний Portfolio счёта содержит итоги в RUB и USD, включая Cash Balance в USD
- **THEN** ответ содержит одну доступную `portfolioSummary` с отдельными Money-итогами RUB и USD без FX-конвертации, общего итога или отдельного итога Cash Balance
#### Scenario: Сводка одного счёта временно недоступна #### Scenario: Сводка одного счёта временно недоступна
- **WHEN** подтверждённый список содержит счёт без последнего доступного Portfolio - **WHEN** подтверждённый список содержит счёт без последнего доступного Portfolio
- **THEN** система отвечает `200` всеми счетами в исходном порядке и передаёт для этого счёта `portfolioSummary: null` - **THEN** система отвечает `200` всеми счетами в исходном порядке и передаёт для этого счёта `portfolioSummary: null`
#### Scenario: Доступный Portfolio пуст #### Scenario: Доступный Portfolio пуст
- **WHEN** последний доступный Portfolio счёта не имеет ненулевых позиций и доступных денежных остатков - **WHEN** последний доступный Portfolio счёта не имеет ненулевых позиций и доступных денежных остатков
- **THEN** система передаёт доступную `portfolioSummary` с пустым `totalsByCurrency` - **THEN** система передаёт доступную `portfolioSummary` без итоговой суммы позиций и денежных остатков
### Requirement: Состояния недоступности списка счетов ### Requirement: Состояния недоступности списка счетов
Система MUST читать подтверждённый список текущего подключения локально. При отсутствии T-Bank connection система MUST отвечать `409` Problem Details с code `TBANK_NOT_CONNECTED`. Если T-Bank отклонил сохранённый token как неаутентифицированный или без прав во время часового обхода, система MUST отвечать `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохранять connection. Временная ошибка обхода MUST сохранять последний подтверждённый список доступным; до первого подтверждённого списка система MUST обозначать продолжающуюся первую загрузку и MUST NOT возвращать частичный список как полный. Система MUST читать подтверждённый список текущего подключения локально. При отсутствии T-Bank connection система MUST отвечать `409` Problem Details с code `TBANK_NOT_CONNECTED`. Если T-Bank отклонил сохранённый token как неаутентифицированный или без прав во время часового обхода, система MUST отвечать `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохранять connection. Временная ошибка обхода MUST сохранять последний подтверждённый список доступным; до первого подтверждённого списка система MUST обозначать продолжающуюся первую загрузку и MUST NOT возвращать частичный список как полный.
@@ -86,10 +82,8 @@ Cached Snapshot MUST оставаться видимым во время обн
клавиатурное состояние. После успешной замены или удаления T-Bank connection клавиатурное состояние. После успешной замены или удаления T-Bank connection
клиент MUST инвалидировать список счетов. клиент MUST инвалидировать список счетов.
Для доступной `portfolioSummary` карточка MUST показывать не более двух Для доступной `portfolioSummary` карточка MUST показывать один Money-итог в RUB.
Money-итогов в исходных валютах и обозначать наличие остальных валют, если Она MUST NOT показывать Cash Balance как отдельный второй итог.
они есть; она MUST NOT показывать или подразумевать FX-конвертацию,
межвалютный агрегированный итог либо Cash Balance как отдельный второй итог.
При `portfolioSummary: null` карточка MUST показывать локальное нейтральное При `portfolioSummary: null` карточка MUST показывать локальное нейтральное
состояние недоступности сводки, сохраняя метаданные счёта и возможность открыть состояние недоступности сводки, сохраняя метаданные счёта и возможность открыть
его. При доступной пустой сводке карточка MUST не выдавать отсутствие итогов его. При доступной пустой сводке карточка MUST не выдавать отсутствие итогов
@@ -127,16 +121,12 @@ Money-итогов в исходных валютах и обозначать н
- **WHEN** обновление устаревшего списка завершается временной ошибкой при наличии последнего успешного Client Cached Snapshot - **WHEN** обновление устаревшего списка завершается временной ошибкой при наличии последнего успешного Client Cached Snapshot
- **THEN** клиент сохраняет snapshot видимым без предупреждения и ручного повтора до исчерпания автоматических повторов и восстанавливает получение данных автоматически - **THEN** клиент сохраняет snapshot видимым без предупреждения и ручного повтора до исчерпания автоматических повторов и восстанавливает получение данных автоматически
#### Scenario: Карточка ограничивает валютные итоги
- **WHEN** доступная сводка счёта содержит итоги в трёх или более валютах
- **THEN** карточка показывает не более двух исходных валютных итогов и обозначает наличие остальных без общего сконвертированного значения
#### Scenario: Недоступность сводки локальна карточке #### Scenario: Недоступность сводки локальна карточке
- **WHEN** клиент получает список, в котором `portfolioSummary` одного счёта равна `null` - **WHEN** клиент получает список, в котором `portfolioSummary` одного счёта равна `null`
- **THEN** он показывает нейтральную недоступность только этой сводки, не скрывает остальные карточки и сохраняет переход на детали счёта - **THEN** он показывает нейтральную недоступность только этой сводки, не скрывает остальные карточки и сохраняет переход на детали счёта
#### Scenario: Пустая сводка не является ошибкой #### Scenario: Пустая сводка не является ошибкой
- **WHEN** клиент получает доступную `portfolioSummary` с пустым `totalsByCurrency` - **WHEN** клиент получает доступную `portfolioSummary` без итоговой суммы
- **THEN** он показывает нейтральное состояние отсутствия финансовых итогов, а не состояние временной ошибки - **THEN** он показывает нейтральное состояние отсутствия финансовых итогов, а не состояние временной ошибки
### Requirement: Клиентские состояния ошибки списка счетов ### Requirement: Клиентские состояния ошибки списка счетов
@@ -17,11 +17,11 @@
- **THEN** система не включает её в результаты скринера независимо от остальных совпавших условий - **THEN** система не включает её в результаты скринера независимо от остальных совпавших условий
### Requirement: Условия скринера применяются совместно ### Requirement: Условия скринера применяются совместно
Система SHALL позволять независимо задавать необязательные условия по сектору, типу акции, валюте расчётов, площадке, стране риска и максимальному количеству бумаг в лоте, а также необязательный текстовый запрос по названию или тикеру. После нажатия «Найти» система SHALL возвращать только акции, удовлетворяющие всем заданным условиям. Отсутствующее или неизвестное значение выбранного условия MUST NOT считаться совпадением. Система SHALL позволять независимо задавать необязательные условия по сектору, типу акции, площадке, стране риска и максимальному количеству бумаг в лоте, а также необязательный текстовый запрос по названию или тикеру. После нажатия «Найти» система SHALL возвращать только акции, удовлетворяющие всем заданным условиям. Отсутствующее или неизвестное значение выбранного условия MUST NOT считаться совпадением.
#### Scenario: Несколько условий без текста #### Scenario: Несколько условий без текста
- **WHEN** пользователь выбрал сектор и валюту, оставил текст пустым и нажал «Найти» - **WHEN** пользователь выбрал сектор, оставил текст пустым и нажал «Найти»
- **THEN** в результате остаются только доступные к покупке акции выбранного сектора и валюты - **THEN** в результате остаются только доступные к покупке акции выбранного сектора
#### Scenario: Текст совместно с характеристиками #### Scenario: Текст совместно с характеристиками
- **WHEN** пользователь ввёл название или тикер, выбрал тип акции и нажал «Найти» - **WHEN** пользователь ввёл название или тикер, выбрал тип акции и нажал «Найти»
@@ -1,7 +1,7 @@
# portfolio-concentration Specification # portfolio-concentration Specification
## Purpose ## Purpose
Позволяет авторизованному User увидеть на странице одного Brokerage Account, какие акции, облигации, секторы и эмитенты занимают наибольшую часть текущей оценки Portfolio в выбранной валюте, без валютной конвертации. Позволяет авторизованному User увидеть на странице одного Brokerage Account, какие акции, облигации, секторы и эмитенты занимают наибольшую часть текущей оценки Portfolio в рублях (RUB).
## Requirements ## Requirements
@@ -41,29 +41,29 @@
- **THEN** показан только разрез «Акции» и доступны переключатели «Акции», «Облигации», «Секторы», «Эмитенты» - **THEN** показан только разрез «Акции» и доступны переключатели «Акции», «Облигации», «Секторы», «Эмитенты»
#### Scenario: Переключение разреза #### Scenario: Переключение разреза
- **GIVEN** открыт разрез «Акции» с выбранной валютой USD - **GIVEN** открыт разрез «Акции»
- **WHEN** User переключает разрез на «Секторы» - **WHEN** User переключает разрез на «Секторы»
- **THEN** показан только разрез «Секторы» в валюте USD - **THEN** показан только разрез «Секторы»
### Requirement: Начальное состояние вкладки ### Requirement: Начальное состояние вкладки
Система SHALL открывать вкладку «Концентрация» при новом заходе на страницу счёта и после перезагрузки с разрезом «Акции» и первой валютой списка. Новый заход — открытие страницы после ухода с неё либо переключение счёта. Пока User остаётся на странице, включая переключение её вкладок, разрез и валюта SHALL сохраняться. Система SHALL открывать вкладку «Концентрация» при новом заходе на страницу счёта и после перезагрузки с разрезом «Акции». Новый заход — открытие страницы после ухода с неё либо переключение счёта. Пока User остаётся на странице, включая переключение её вкладок, разрез SHALL сохраняться.
**Источник (ненормативно):** [3081](https://git.personal.ksv741.ru/WealthLedger/main/issues/48#issuecomment-3081) **Источник (ненормативно):** [3081](https://git.personal.ksv741.ru/WealthLedger/main/issues/48#issuecomment-3081)
#### Scenario: Возврат с другой вкладки страницы #### Scenario: Возврат с другой вкладки страницы
- **GIVEN** на вкладке выбраны разрез «Секторы» и валюта USD - **GIVEN** на вкладке выбран разрез «Секторы»
- **WHEN** User переключается на вкладку «Позиции» и возвращается на «Концентрация» - **WHEN** User переключается на вкладку «Позиции» и возвращается на «Концентрация»
- **THEN** выбраны разрез «Секторы» и валюта USD - **THEN** выбран разрез «Секторы»
#### Scenario: Повторный заход #### Scenario: Повторный заход
- **GIVEN** User выбрал разрез «Секторы» и валюту USD, затем покинул страницу счёта - **GIVEN** User выбрал разрез «Секторы», затем покинул страницу счёта
- **WHEN** User снова открывает страницу счёта и вкладку «Концентрация» - **WHEN** User снова открывает страницу счёта и вкладку «Концентрация»
- **THEN** открыт разрез «Акции» с первой валютой списка - **THEN** открыт разрез «Акции»
#### Scenario: Перезагрузка #### Scenario: Перезагрузка
- **GIVEN** на вкладке выбраны разрез «Эмитенты» и валюта USD - **GIVEN** на вкладке выбран разрез «Эмитенты»
- **WHEN** User перезагружает страницу - **WHEN** User перезагружает страницу
- **THEN** после открытия вкладки показаны разрез «Акции» и первая валюта списка - **THEN** после открытия вкладки показан разрез «Акции»
### Requirement: Состав разрезов ### Requirement: Состав разрезов
Элементы разреза SHALL формироваться из позиций типов по таблице; другие типы, включая `CURRENCY`, SHALL NOT входить во вкладку. Элементы разреза SHALL формироваться из позиций типов по таблице; другие типы, включая `CURRENCY`, SHALL NOT входить во вкладку.
@@ -111,7 +111,7 @@
- **THEN** показаны два отдельных элемента «Альфа» - **THEN** показаны два отдельных элемента «Альфа»
### Requirement: Название эмитента при разных названиях бренда ### Requirement: Название эмитента при разных названиях бренда
Если учитываемые позиции одного `issuer.id` имеют разные `issuer.name`, название элемента SHALL быть `issuer.name` позиции с наибольшим `currentValue` в выбранной валюте. При равных значениях SHALL выбираться название, меньшее по алфавитному порядку требования «Порядок элементов с равной суммой». Если учитываемые позиции одного `issuer.id` имеют разные `issuer.name`, название элемента SHALL быть `issuer.name` позиции с наибольшим `currentValue`. При равных значениях SHALL выбираться название, меньшее по алфавитному порядку требования «Порядок элементов с равной суммой».
**Источник (ненормативно):** [#48](https://git.personal.ksv741.ru/WealthLedger/main/issues/48#issuecomment-3081), [#48](https://git.personal.ksv741.ru/WealthLedger/main/issues/48#issuecomment-3082) **Источник (ненормативно):** [#48](https://git.personal.ksv741.ru/WealthLedger/main/issues/48#issuecomment-3081), [#48](https://git.personal.ksv741.ru/WealthLedger/main/issues/48#issuecomment-3082)
@@ -126,7 +126,7 @@
- **THEN** элемент назван «Сбер» - **THEN** элемент назван «Сбер»
### Requirement: Позиции, учитываемые в разрезе ### Requirement: Позиции, учитываемые в разрезе
Система SHALL учитывать позицию в ранжировании, долях и диаграмме только при `currentValue` больше нуля в выбранной валюте; остальные позиции SHALL NOT учитываться. Позиция с нулевым `currentValue` не показывается и не считается неучтённой, позиция другой валюты относится к другой валюте выбора. Система SHALL учитывать позицию в ранжировании, долях и диаграмме только при `currentValue` больше нуля; остальные позиции SHALL NOT учитываться. Позиция с нулевым `currentValue` не показывается и не считается неучтённой.
**Источник (ненормативно):** [#48](https://git.personal.ksv741.ru/WealthLedger/main/issues/48#issuecomment-3078) **Источник (ненормативно):** [#48](https://git.personal.ksv741.ru/WealthLedger/main/issues/48#issuecomment-3078)
@@ -136,12 +136,12 @@
- **THEN** сумма разреза 1 000 ₽, доли 60 % и 40 % - **THEN** сумма разреза 1 000 ₽, доли 60 % и 40 %
#### Scenario: Позиция с нулевой стоимостью #### Scenario: Позиция с нулевой стоимостью
- **GIVEN** есть акции на 100 и 0 в выбранной валюте - **GIVEN** есть акции на 100 и 0 в RUB
- **WHEN** User открывает разрез «Акции» - **WHEN** User открывает разрез «Акции»
- **THEN** показана только акция на 100 с долей 100 %, сноски нет - **THEN** показана только акция на 100 с долей 100 %, сноски нет
### Requirement: Сноска о не учтённых позициях ### Requirement: Сноска о не учтённых позициях
Система SHALL показывать сноску «Не учтено позиций: N» при N больше нуля. N — число позиций разреза с `currentValue` `null` (в любой валюте) либо отрицательным в выбранной. Система SHALL показывать сноску «Не учтено позиций: N» при N больше нуля. N — число позиций разреза с `currentValue` `null` либо отрицательным.
**Источник (ненормативно):** [3080](https://git.personal.ksv741.ru/WealthLedger/main/issues/48#issuecomment-3080). Примечание: при неполных данных «Структура портфеля» показывает «Доля недоступна», вкладка — доли по учтённым ([3078](https://git.personal.ksv741.ru/WealthLedger/main/issues/48#issuecomment-3078)). **Источник (ненормативно):** [3080](https://git.personal.ksv741.ru/WealthLedger/main/issues/48#issuecomment-3080). Примечание: при неполных данных «Структура портфеля» показывает «Доля недоступна», вкладка — доли по учтённым ([3078](https://git.personal.ksv741.ru/WealthLedger/main/issues/48#issuecomment-3078)).
@@ -150,81 +150,11 @@
- **WHEN** User открывает разрез «Акции» в RUB - **WHEN** User открывает разрез «Акции» в RUB
- **THEN** показана сноска «Не учтено позиций: 2» - **THEN** показана сноска «Не учтено позиций: 2»
#### Scenario: Неоценённая позиция при смене валюты
- **GIVEN** есть акции на 100 USD и 200 RUB и одна акция без `currentValue`
- **WHEN** User выбирает USD
- **THEN** показаны акция на 100 $ с долей 100 % и сноска «Не учтено позиций: 1»
#### Scenario: Отрицательная позиция другой валюты
- **GIVEN** есть акции на 100 RUB и -50 USD
- **WHEN** User открывает разрез «Акции» в RUB
- **THEN** сноски нет
#### Scenario: Нет не учтённых позиций #### Scenario: Нет не учтённых позиций
- **GIVEN** все акции имеют положительную стоимость - **GIVEN** все акции имеют положительную стоимость
- **WHEN** User открывает разрез «Акции» - **WHEN** User открывает разрез «Акции»
- **THEN** сноски нет - **THEN** сноски нет
### Requirement: Одна валюта за раз
Система SHALL показывать значения вкладки в одной выбранной валюте, общей для всех разрезов и сохраняемой при их переключении, и при нескольких доступных валютах предоставлять выбор из них. Смена валюты SHALL пересчитывать разрез по позициям этой валюты. Система SHALL NOT конвертировать валюты и показывать общий итог нескольких валют.
**Источник (ненормативно):** [#48](https://git.personal.ksv741.ru/WealthLedger/main/issues/48#issuecomment-3078)
#### Scenario: Смена валюты
- **GIVEN** акции стоят 1 000 RUB и 100 USD, выбрана RUB
- **WHEN** User выбирает USD
- **THEN** разрез «Акции» показывает только акции в USD, сумму 100 $ и долю 100 %, без рублёвых значений
#### Scenario: Валюта сохраняется при смене разреза
- **GIVEN** выбрана USD в разрезе «Акции»
- **WHEN** User переключает разрез на «Облигации»
- **THEN** показаны облигации в USD
### Requirement: Доступные валюты вкладки
Доступные валюты — валюты ненулевых `currentValue` позиций `STOCK`, `BOND` и `FUND`, включая отрицательные (для сноски). Валюта только с нулевой стоимостью в список не входит. Cash Balance и другие типы валюты не определяют. Список упорядочен по ISO-коду, сначала выбрана первая. При одной валюте или без валют выбор SHALL NOT показываться.
**Источник (ненормативно):** [#48, подтверждено](https://git.personal.ksv741.ru/WealthLedger/main/issues/48#issuecomment-3080)
#### Scenario: Начальная валюта
- **GIVEN** акции оценены в USD, облигации в RUB, а Cash Balance есть ещё в EUR и позиция `CURRENCY` оценена в CHF
- **WHEN** User впервые открывает вкладку «Концентрация»
- **THEN** в выборе RUB и USD, выбрана RUB, EUR и CHF в выборе нет
#### Scenario: Единственная валюта
- **GIVEN** все оценённые акции, облигации и фонды стоят в RUB
- **WHEN** User открывает вкладку «Концентрация»
- **THEN** выбора валюты нет, значения показаны в RUB
#### Scenario: Валюта только у позиции с нулевой стоимостью
- **GIVEN** акции стоят 100 RUB, а единственная позиция в USD — акция со стоимостью 0
- **WHEN** User открывает вкладку «Концентрация»
- **THEN** в списке только RUB, USD в нём нет
#### Scenario: Валюта только у отрицательной позиции
- **GIVEN** акции стоят 100 RUB, а единственная позиция в USD — акция со стоимостью -50
- **WHEN** User выбирает USD в разрезе «Акции»
- **THEN** показаны пустое состояние и сноска «Не учтено позиций: 1»
#### Scenario: Нет оценённых позиций
- **GIVEN** у всех позиций `STOCK`, `BOND` и `FUND` `currentValue` равен `null`
- **WHEN** User открывает вкладку «Концентрация»
- **THEN** выбора валюты нет, разрез показывает пустое состояние без валюты и сноску с числом таких позиций
### Requirement: Независимый выбор валюты вкладки
Выбор валюты вкладки «Концентрация» SHALL быть независим от выбора валюты в «Структуре портфеля»: смена валюты в одном месте SHALL NOT менять валюту в другом. Если выбранная валюта вкладки пропала из списка после обновления Portfolio, система SHALL выбрать первую валюту списка.
**Источник (ненормативно):** [#48, подтверждено](https://git.personal.ksv741.ru/WealthLedger/main/issues/48#issuecomment-3080)
#### Scenario: Структура портфеля не влияет
- **GIVEN** в «Структуре портфеля» выбрана USD, а на вкладке «Концентрация» выбрана RUB
- **WHEN** User меняет валюту в «Структуре портфеля» на RUB и обратно
- **THEN** выбранная на вкладке валюта остаётся RUB
#### Scenario: Валюта пропала после обновления
- **GIVEN** на вкладке выбрана USD
- **WHEN** обновлённый Portfolio не содержит позиций `STOCK`, `BOND` и `FUND` в USD
- **THEN** выбрана первая валюта обновлённого списка
### Requirement: Топ-5 и строка «Прочее» ### Requirement: Топ-5 и строка «Прочее»
Система SHALL упорядочивать элементы разреза по сумме по убыванию. Если элементов не больше шести, каждый SHALL показываться строкой с названием, суммой и долей. Если семь и больше, SHALL показываться пять первых строк и строка «Прочее» с суммой, долей и числом остальных элементов («2 элемента», «5 элементов»). Система SHALL упорядочивать элементы разреза по сумме по убыванию. Если элементов не больше шести, каждый SHALL показываться строкой с названием, суммой и долей. Если семь и больше, SHALL показываться пять первых строк и строка «Прочее» с суммой, долей и числом остальных элементов («2 элемента», «5 элементов»).
@@ -251,7 +181,7 @@
- **THEN** показаны три строки, строки «Прочее» нет - **THEN** показаны три строки, строки «Прочее» нет
### Requirement: Раскрытие строки «Прочее» ### Requirement: Раскрытие строки «Прочее»
По нажатию на «Прочее», в том числе с клавиатуры, система SHALL раскрывать полный список остальных элементов в порядке ранжирования с суммой и долей; повторное нажатие сворачивает его. Раскрытие SHALL NOT менять пять первых строк и диаграмму. При показе разреза и смене валюты «Прочее» SHALL быть свёрнуто. По нажатию на «Прочее», в том числе с клавиатуры, система SHALL раскрывать полный список остальных элементов в порядке ранжирования с суммой и долей; повторное нажатие сворачивает его. Раскрытие SHALL NOT менять пять первых строк и диаграмму. При показе разреза «Прочее» SHALL быть свёрнуто.
**Источник (ненормативно):** [#48](https://git.personal.ksv741.ru/WealthLedger/main/issues/48#issuecomment-3077) **Источник (ненормативно):** [#48](https://git.personal.ksv741.ru/WealthLedger/main/issues/48#issuecomment-3077)
@@ -265,11 +195,6 @@
- **WHEN** User нажимает на него ещё раз - **WHEN** User нажимает на него ещё раз
- **THEN** список остальных элементов скрыт - **THEN** список остальных элементов скрыт
#### Scenario: Смена валюты сворачивает «Прочее»
- **GIVEN** «Прочее» раскрыто в RUB
- **WHEN** User выбирает USD
- **THEN** «Прочее» свёрнуто
### Requirement: Порядок элементов с равной суммой ### Requirement: Порядок элементов с равной суммой
Элементы разреза с равной суммой SHALL упорядочиваться по названию по возрастанию без учёта регистра: сначала названия, начинающиеся с цифры, затем с латинской буквы, затем с кириллической; внутри группы — по алфавиту, при совпадении названия — по порядку первой позиции элемента в Portfolio. Элементы разреза с равной суммой SHALL упорядочиваться по названию по возрастанию без учёта регистра: сначала названия, начинающиеся с цифры, затем с латинской буквы, затем с кириллической; внутри группы — по алфавиту, при совпадении названия — по порядку первой позиции элемента в Portfolio.
@@ -286,7 +211,7 @@
- **THEN** порядок строк: «VK», «X5», «Сбер» - **THEN** порядок строк: «VK», «X5», «Сбер»
### Requirement: Расчёт долей ### Requirement: Расчёт долей
Точная доля элемента — сумма его учитываемых позиций, делённая на сумму всех учитываемых позиций разреза в выбранной валюте; Cash Balance в расчёте не участвует. Точные доли элементов SHALL давать в сумме ровно 100 %, точная доля «Прочее» — равняться сумме точных долей остальных элементов, суммы — быть без округления. Точная доля элемента — сумма его учитываемых позиций, делённая на сумму всех учитываемых позиций разреза; Cash Balance в расчёте не участвует. Точные доли элементов SHALL давать в сумме ровно 100 %, точная доля «Прочее» — равняться сумме точных долей остальных элементов, суммы — быть без округления.
**Источник (ненормативно):** [#48](https://git.personal.ksv741.ru/WealthLedger/main/issues/48#issuecomment-3078) **Источник (ненормативно):** [#48](https://git.personal.ksv741.ru/WealthLedger/main/issues/48#issuecomment-3078)
@@ -411,7 +336,7 @@
- **THEN** диаграмма состоит из шести сегментов с долями 30 %, 25 %, 20 %, 15 %, 6 %, 4 % - **THEN** диаграмма состоит из шести сегментов с долями 30 %, 25 %, 20 %, 15 %, 6 %, 4 %
### Requirement: Центр диаграммы ### Requirement: Центр диаграммы
Система SHALL показывать в центре диаграммы сумму разреза — точную сумму учитываемых позиций в выбранной валюте, а над ней, когда показывается метрика «доля топ-5», подпись «Топ-5 · N %», где N — доля в формате «Показ долей». Без метрики показывается только сумма. Эти значения SHALL входить в текстовый эквивалент диаграммы. Система SHALL показывать в центре диаграммы сумму разреза — точную сумму учитываемых позиций в RUB, а над ней, когда показывается метрика «доля топ-5», подпись «Топ-5 · N %», где N — доля в формате «Показ долей». Без метрики показывается только сумма. Эти значения SHALL входить в текстовый эквивалент диаграммы.
**Источник (ненормативно):** [#48](https://git.personal.ksv741.ru/WealthLedger/main/issues/48#issuecomment-3081) **Источник (ненормативно):** [#48](https://git.personal.ksv741.ru/WealthLedger/main/issues/48#issuecomment-3081)
@@ -447,12 +372,12 @@
- **THEN** доступны название, сумма и доля каждого сегмента в порядке списка, сопоставление со строкой не требует различения цветов - **THEN** доступны название, сумма и доля каждого сегмента в порядке списка, сопоставление со строкой не требует различения цветов
### Requirement: Пустое состояние разреза ### Requirement: Пустое состояние разреза
Если выбранный разрез не содержит учитываемых элементов, система SHALL показывать пустое состояние с названием разреза и выбранной валютой (если валюты доступны) вместо списка, суммарной доли и диаграммы. Выбор валюты, если он показывается, и сноска при N больше нуля SHALL оставаться доступными. Если выбранный разрез не содержит учитываемых элементов, система SHALL показывать пустое состояние с названием разреза вместо списка, суммарной доли и диаграммы. Сноска при N больше нуля SHALL оставаться доступной.
#### Scenario: Нет облигаций в выбранной валюте #### Scenario: Нет облигаций
- **GIVEN** есть акции в RUB и облигации только в USD - **GIVEN** есть акции в RUB, облигаций нет
- **WHEN** User открывает разрез «Облигации» в RUB - **WHEN** User открывает разрез «Облигации»
- **THEN** показано пустое состояние, выбор валюты доступен (валют две) - **THEN** показано пустое состояние с названием разреза
#### Scenario: Все позиции разреза отрицательные #### Scenario: Все позиции разреза отрицательные
- **GIVEN** в RUB есть только одна акция со стоимостью -50 - **GIVEN** в RUB есть только одна акция со стоимостью -50
@@ -1,18 +1,18 @@
# portfolio-value-history Specification # portfolio-value-history Specification
## Purpose ## Purpose
Определяет сохранение точных оценок Portfolio по счёту и валюте для будущей истории без предоставления публичного чтения этих точек. Определяет сохранение точных оценок Portfolio по счёту в RUB для будущей истории без предоставления публичного чтения этих точек.
## Requirements ## Requirements
### Requirement: Полные оценки Portfolio сохраняются как история ### Requirement: Полные оценки Portfolio сохраняются как история
Система SHALL сохранять для каждого доступного Brokerage Account точку оценки по каждой исходной валюте только после успешного получения полного Portfolio с вычислимой стоимостью всех его позиций. Точка MUST содержать точное значение без валютной конвертации и фактическое время успешного получения Portfolio. Система MUST NOT подставлять ноль за отсутствующую стоимость и MUST NOT создавать точку для неполной оценки. Повторная публикация одной попытки MUST NOT создавать дубликат точки. Система SHALL сохранять для каждого доступного Brokerage Account точку оценки в RUB только после успешного получения полного Portfolio с вычислимой стоимостью всех его позиций. Точка MUST содержать точное значение и фактическое время успешного получения Portfolio. Система MUST NOT подставлять ноль за отсутствующую стоимость и MUST NOT создавать точку для неполной оценки. Повторная публикация одной попытки MUST NOT создавать дубликат точки.
Точки, сохранённые до исправления двойного учёта `CURRENCY`/Cash Balance (WealthLedger/backend#140), сброшены миграцией. История оценок начинается заново после её применения; старые точки MUST NOT показываться как сопоставимые с новыми. Точки, сохранённые до исправления двойного учёта `CURRENCY`/Cash Balance (WealthLedger/backend#140), сброшены миграцией. История оценок начинается заново после её применения; старые точки MUST NOT показываться как сопоставимые с новыми.
#### Scenario: Полная мультивалютная оценка #### Scenario: Полная оценка в RUB
- **WHEN** успешный полный Portfolio содержит вычислимые итоги RUB и USD - **WHEN** успешный полный Portfolio содержит вычислимый итог в RUB
- **THEN** система сохраняет две отдельные точки с исходными значениями и валютами - **THEN** система сохраняет одну точку с точным значением в RUB
#### Scenario: Неполная оценка #### Scenario: Неполная оценка
- **WHEN** в новом полном составе Portfolio у позиции отсутствует вычислимая стоимость - **WHEN** в новом полном составе Portfolio у позиции отсутствует вычислимая стоимость
+67 -152
View File
@@ -2,12 +2,12 @@
## Purpose ## Purpose
Позволяет авторизованному User просматривать нормализованный текущий состав и агрегаты Portfolio одного доступного Brokerage Account без валютной конвертации. Позволяет авторизованному User просматривать нормализованный текущий состав и агрегаты Portfolio одного доступного Brokerage Account в рублях (RUB).
## Requirements ## Requirements
### Requirement: Получение Portfolio выбранного Brokerage Account ### 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 }`: `data` MUST содержать сохранённый полный Portfolio Snapshot с `lastSuccessfulAt` в RFC 3339 UTC без миллисекунд, boolean `isStale`, `totalsByCurrency`, `positions` и `cashBalances`; `meta` MUST содержать наблюдаемое состояние resource по `tbank-data-sync`, включая freshness, полноту, время последнего успешного Snapshot, последнюю часовую попытку и доступность. Обычный GET MUST читать сохранённый Snapshot и его наблюдаемое состояние без ожидания T-Bank и MUST NOT запускать обновление личного Portfolio. Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts/:accountId/portfolio`. Она MUST возвращать нормализованный полный Portfolio Snapshot только для открытого поддерживаемого Brokerage Account, доступного через текущий T-Bank connection User. Ответ MUST иметь envelope `{ data, meta }`: `data` MUST содержать сохранённый полный Portfolio Snapshot с `lastSuccessfulAt` в RFC 3339 UTC без миллисекунд, boolean `isStale`, итоговую сумму Portfolio в RUB, `positions` и Cash Balance; `meta` MUST содержать наблюдаемое состояние resource по `tbank-data-sync`, включая freshness, полноту, время последнего успешного Snapshot, последнюю часовую попытку и доступность. Обычный GET MUST читать сохранённый Snapshot и его наблюдаемое состояние без ожидания T-Bank и MUST NOT запускать обновление личного Portfolio.
Portfolio Snapshot является результатом наблюдения Portfolio на конкретный момент. Он MUST NOT означать, что WealthLedger хранит Portfolio как долговременный агрегат или подтверждает доступность Brokerage Account вне текущего T-Bank connection. Portfolio Snapshot является результатом наблюдения Portfolio на конкретный момент. Он MUST NOT означать, что WealthLedger хранит Portfolio как долговременный агрегат или подтверждает доступность Brokerage Account вне текущего T-Bank connection.
@@ -20,7 +20,7 @@ Portfolio Snapshot является результатом наблюдения
- **THEN** система отвечает `404` без Portfolio Snapshot - **THEN** система отвечает `404` без Portfolio Snapshot
### Requirement: Нормализованный состав Portfolio ### Requirement: Нормализованный состав Portfolio
Система MUST возвращать только обычные Portfolio Position с ненулевым количеством и доступные Cash Balance с ненулевой суммой. Cash Balance MUST быть отдельным от Portfolio Position элементом и агрегироваться до одного значения на валюту. Portfolio Position с типом `CURRENCY` MUST сохранять исходное количество и идентификаторы для контракта, но отражает тот же кэш, что и Cash Balance, и MUST NOT повторно учитываться в валютных итогах и структуре. Клиент MUST показывать этот кэш один раз как Cash Balance в разделе «Деньги», без отдельной карточки `CURRENCY`. Порядок Portfolio Position и Cash Balance MUST соответствовать upstream-порядку; система MUST NOT сортировать или объединять Portfolio Position с одинаковым FIGI. Виртуальные позиции и заблокированные денежные остатки MUST NOT входить в Portfolio. Система MUST возвращать только обычные Portfolio Position с ненулевым количеством и доступные Cash Balance с ненулевой суммой. Cash Balance MUST быть отдельным от Portfolio Position элементом и агрегироваться в один остаток в RUB. Portfolio Position с типом `CURRENCY` MUST сохранять исходное количество и идентификаторы для контракта, но отражает тот же кэш, что и Cash Balance, и MUST NOT повторно учитываться в итоге и структуре. Позиции, денежные остатки и данные инструментов в валютах, отличных от RUB, MUST отфильтровываться при получении из T-Bank и MUST NOT входить в Portfolio, его итог, структуру или любые другие агрегаты. Клиент MUST показывать этот кэш один раз как Cash Balance в разделе «Деньги», без отдельной карточки `CURRENCY`. Порядок 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` — имя файла логотипа бренда инструмента, nullable `sector` — сектор инструмента и nullable `issuer` — эмитент позиции. Имя файла логотипа MUST заполняться из metadata инструмента, когда upstream предоставляет бренд с непустым именем файла логотипа; во всех остальных случаях оно MUST быть `null`. Логотип является presentation metadata: он не влияет на количество, стоимость, агрегаты, успешность или свежесть Portfolio Snapshot. Поле `sector` позиции типа `STOCK`, `BOND` или `FUND` MUST содержать значение сектора из metadata инструмента в том виде, как его определяет T-Bank, без перекодирования и собственной классификации, с обрезанными пробелами в начале и конце; значение из одних пробелов считается отсутствующим. Во всех остальных случаях, включая остальные типы позиций, оно MUST быть `null`. Поле `issuer` позиции типа `STOCK`, `BOND` или `FUND`, включая фонд, MUST быть объектом с непустыми строками `id` — идентификатор бренда актива инструмента — и `name` — название этого бренда (не название компании), когда T-Bank предоставляет для инструмента бренд актива с идентификатором и названием, непустыми после обрезки пробелов в начале и конце; значение из одних пробелов считается отсутствующим. Во всех остальных случаях, включая остальные типы позиций, оно MUST быть `null`. Система MUST NOT определять эмитента по совпадению названия, тикера или ISIN. Сектор и эмитент не влияют на количество, стоимость, агрегаты, успешность или свежесть Portfolio Snapshot: их отсутствие либо недоступность MUST NOT делать Snapshot неуспешным или устаревшим и MUST NOT скрывать позицию или её финансовые значения, кроме отказа token по требованию «Свежесть и fallback полного Portfolio Snapshot». Система MUST искать metadata по instrument UID при его наличии, затем по FIGI. Отображаемый идентификатор MUST предпочитать name, затем ticker, а при недоступности metadata использовать FIGI, не скрывая финансовые значения. Каждая 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` — имя файла логотипа бренда инструмента, nullable `sector` — сектор инструмента и nullable `issuer` — эмитент позиции. Имя файла логотипа MUST заполняться из metadata инструмента, когда upstream предоставляет бренд с непустым именем файла логотипа; во всех остальных случаях оно MUST быть `null`. Логотип является presentation metadata: он не влияет на количество, стоимость, агрегаты, успешность или свежесть Portfolio Snapshot. Поле `sector` позиции типа `STOCK`, `BOND` или `FUND` MUST содержать значение сектора из metadata инструмента в том виде, как его определяет T-Bank, без перекодирования и собственной классификации, с обрезанными пробелами в начале и конце; значение из одних пробелов считается отсутствующим. Во всех остальных случаях, включая остальные типы позиций, оно MUST быть `null`. Поле `issuer` позиции типа `STOCK`, `BOND` или `FUND`, включая фонд, MUST быть объектом с непустыми строками `id` — идентификатор бренда актива инструмента — и `name` — название этого бренда (не название компании), когда T-Bank предоставляет для инструмента бренд актива с идентификатором и названием, непустыми после обрезки пробелов в начале и конце; значение из одних пробелов считается отсутствующим. Во всех остальных случаях, включая остальные типы позиций, оно MUST быть `null`. Система MUST NOT определять эмитента по совпадению названия, тикера или ISIN. Сектор и эмитент не влияют на количество, стоимость, агрегаты, успешность или свежесть Portfolio Snapshot: их отсутствие либо недоступность MUST NOT делать Snapshot неуспешным или устаревшим и MUST NOT скрывать позицию или её финансовые значения, кроме отказа token по требованию «Свежесть и fallback полного Portfolio Snapshot». Система MUST искать metadata по instrument UID при его наличии, затем по FIGI. Отображаемый идентификатор MUST предпочитать name, затем ticker, а при недоступности metadata использовать FIGI, не скрывая финансовые значения.
@@ -68,13 +68,17 @@ Portfolio Snapshot является результатом наблюдения
- **WHEN** у инструмента позиции нет бренда, имя файла логотипа пустое либо metadata недоступна - **WHEN** у инструмента позиции нет бренда, имя файла логотипа пустое либо metadata недоступна
- **THEN** система возвращает позицию с `logoName: null`, не скрывая финансовые значения - **THEN** система возвращает позицию с `logoName: null`, не скрывая финансовые значения
#### Scenario: Денежные остатки в одной валюте #### Scenario: Несколько денежных остатков в RUB
- **WHEN** upstream возвращает несколько ненулевых доступных денежных остатков одной валюты - **WHEN** upstream возвращает несколько ненулевых доступных денежных остатков в RUB
- **THEN** система возвращает один Cash Balance с их точной суммой - **THEN** система возвращает один Cash Balance с их точной суммой
#### Scenario: Нерублёвые данные не входят в Portfolio
- **WHEN** upstream возвращает позиции, денежные остатки или инструменты в валюте, отличной от RUB
- **THEN** система отфильтровывает их при получении и не возвращает их в `positions`, Cash Balance, итоге, структуре или агрегатах
#### Scenario: Штатный пустой Portfolio #### Scenario: Штатный пустой Portfolio
- **WHEN** у доступного Brokerage Account нет ненулевых обычных позиций и доступных денежных остатков - **WHEN** у доступного Brokerage Account нет ненулевых обычных позиций и доступных денежных остатков
- **THEN** система отвечает `200` с пустыми `positions`, `cashBalances` и `totalsByCurrency` - **THEN** система отвечает `200` с пустыми `positions` и без Cash Balance и итоговой суммы
#### Scenario: Сектор инструмента доступен #### Scenario: Сектор инструмента доступен
- **WHEN** metadata инструмента позиции содержит сектор `energy` - **WHEN** metadata инструмента позиции содержит сектор `energy`
@@ -119,24 +123,20 @@ Portfolio Snapshot является результатом наблюдения
#### Scenario: Недоступность сектора и эмитента не нарушает Snapshot #### Scenario: Недоступность сектора и эмитента не нарушает Snapshot
- **GIVEN** сектор и бренд актива позиции ещё ни разу не получены - **GIVEN** сектор и бренд актива позиции ещё ни разу не получены
- **WHEN** получение сектора и бренда актива завершается ошибкой, а количества и цены позиции получены - **WHEN** получение сектора и бренда актива завершается ошибкой, а количества и цены позиции получены
- **THEN** система сохраняет Snapshot с `sector: null` и `issuer: null` у затронутых позиций; их `currentValue`, `totalsByCurrency`, `lastSuccessfulAt` и `isStale` такие же, как при доступных сектора и бренде - **THEN** система сохраняет Snapshot с `sector: null` и `issuer: null` у затронутых позиций; их `currentValue`, итоговая сумма, `lastSuccessfulAt` и `isStale` такие же, как при доступных сектора и бренде
### Requirement: Точные значения и валютные агрегаты ### Requirement: Точные значения и итоговая сумма в RUB
Все количества и финансовые значения в Portfolio Snapshot MUST передаваться десятичными строками без экспоненциальной записи и без незначащих завершающих нулей. Денежное значение MUST быть единым Money-значением с ISO-кодом валюты и десятичной строкой значения. Система MUST корректно нормализовать отрицательные целую и дробную части upstream-значений. Отсутствующее значение MUST передаваться как `null`, а не как ноль. Все количества и финансовые значения в Portfolio Snapshot MUST передаваться десятичными строками без экспоненциальной записи и без незначащих завершающих нулей. Денежное значение MUST быть Money-значением с ISO-кодом валюты `RUB` и десятичной строкой значения. Система MUST корректно нормализовать отрицательные целую и дробную части upstream-значений. Отсутствующее значение MUST передаваться как `null`, а не как ноль.
`currentValue` Portfolio Position MUST быть точным произведением количества на текущую цену при наличии валидных множителей и валюты цены. `absoluteReturn` MUST быть точной величиной `(currentPrice - averagePurchasePrice) * quantity` только когда обе цены имеют одинаковую известную валюту. Позиция без вычислимой текущей стоимости MUST оставаться в ответе, но MUST NOT входить в агрегаты. `totalsByCurrency` MUST содержать точную сумму currentValue позиций, кроме `CURRENCY`, и Cash Balance в каждой валюте без валютной конвертации. Структура Portfolio MUST учитывать `CURRENCY` только как `CASH` из Cash Balance, без второго вклада. `currentValue` Portfolio Position MUST быть точным произведением количества на текущую цену при наличии валидных множителей. `absoluteReturn` MUST быть точной величиной `(currentPrice - averagePurchasePrice) * quantity` только когда обе цены известны. Позиция без вычислимой текущей стоимости MUST оставаться в ответе, но MUST NOT входить в агрегаты. Итоговая сумма Portfolio MUST быть точной суммой currentValue позиций, кроме `CURRENCY`, и Cash Balance в RUB. Структура Portfolio MUST учитывать `CURRENCY` только как `CASH` из Cash Balance, без второго вклада.
#### Scenario: Валютная позиция отражает денежный остаток #### Scenario: Валютная позиция отражает денежный остаток
- **WHEN** Portfolio содержит `CURRENCY` position стоимостью 100 RUB и Cash Balance 100 RUB - **WHEN** Portfolio содержит `CURRENCY` position стоимостью 100 RUB и Cash Balance 100 RUB
- **THEN** система сохраняет `CURRENCY` position с количеством в `positions`, но возвращает итог 100 RUB и один вклад `CASH` 100 RUB в структуре; клиент показывает 100 RUB в «Деньги» без отдельной валютной карточки - **THEN** система сохраняет `CURRENCY` position с количеством в `positions`, но возвращает итог 100 RUB и один вклад `CASH` 100 RUB в структуре; клиент показывает 100 RUB в «Деньги» без отдельной валютной карточки
#### Scenario: Мультивалютный Portfolio
- **WHEN** Portfolio содержит финансовые значения в RUB и USD
- **THEN** система возвращает отдельные агрегаты RUB и USD без общего сконвертированного итога
#### Scenario: Позиция без вычислимой стоимости #### Scenario: Позиция без вычислимой стоимости
- **WHEN** у ненулевой позиции отсутствует либо невалидна цена, количество или валюта цены - **WHEN** у ненулевой позиции отсутствует либо невалидна цена или количество
- **THEN** система возвращает позицию с `currentValue: null` и не включает её в `totalsByCurrency` - **THEN** система возвращает позицию с `currentValue: null` и не включает её в итоговую сумму
#### Scenario: Отрицательная дробная часть upstream-значения #### Scenario: Отрицательная дробная часть upstream-значения
- **WHEN** upstream возвращает сумму с `units: 0` и отрицательной дробной частью `nano: -500000000` - **WHEN** upstream возвращает сумму с `units: 0` и отрицательной дробной частью `nano: -500000000`
@@ -193,7 +193,7 @@ skeleton MUST NOT быть интерактивным или получать ф
Истечение срока свежести MUST NOT скрывать доступный Client Cached Snapshot. Истечение срока свежести MUST NOT скрывать доступный Client Cached Snapshot.
При обновлении и временной ошибке клиент MUST сохранять данные того же При обновлении и временной ошибке клиент MUST сохранять данные того же
Portfolio, включая валютные итоги, без индикации фоновой работы собственного Portfolio, включая итоговую сумму, без индикации фоновой работы собственного
чтения, технических предупреждений об этом чтении и уведомлений. Исходное чтения, технических предупреждений об этом чтении и уведомлений. Исходное
`lastSuccessfulAt` и `isStale` MUST сохранять внутренний смысл: их показ или `lastSuccessfulAt` и `isStale` MUST сохранять внутренний смысл: их показ или
скрытие MUST NOT делать сохранённый снимок новым успешным результатом или скрытие MUST NOT делать сохранённый снимок новым успешным результатом или
@@ -206,7 +206,7 @@ MUST NOT подменять друг друга.
элементом по `frontend-data-resilience`, не превращая в действие пользователя. элементом по `frontend-data-resilience`, не превращая в действие пользователя.
Клиент MUST показывать штатное empty-состояние только при подтверждённом полном Клиент MUST показывать штатное empty-состояние только при подтверждённом полном
результате с пустыми `positions` и `cashBalances`, без вымышленных нулевых карточек. результате с пустыми `positions` и без Cash Balance, без вымышленных нулевых карточек.
Неполный итог MUST NOT выдаваться за окончательный; доступные независимые данные Неполный итог MUST NOT выдаваться за окончательный; доступные независимые данные
MUST сохраняться на экране. При подтверждённом `404` клиент MUST скрыть личный MUST сохраняться на экране. При подтверждённом `404` клиент MUST скрыть личный
Portfolio и предложить возврат к списку счетов. При `TBANK_NOT_CONNECTED` или Portfolio и предложить возврат к списку счетов. При `TBANK_NOT_CONNECTED` или
@@ -216,7 +216,7 @@ Portfolio и предложить возврат к списку счетов.
#### Scenario: Показ устаревшего Portfolio #### Scenario: Показ устаревшего Portfolio
- **WHEN** клиент получает успешный ответ с `isStale: true` в действующей границе доступа - **WHEN** клиент получает успешный ответ с `isStale: true` в действующей границе доступа
- **THEN** клиент показывает позиции, остатки и доступные валютные итоги и обозначает устаревание источника текстом либо доступно именованным элементом, без ручного обновления - **THEN** клиент показывает позиции, остаток и доступную итоговую сумму и обозначает устаревание источника текстом либо доступно именованным элементом, без ручного обновления
#### Scenario: Неуспешная последняя попытка сохраняет Portfolio #### Scenario: Неуспешная последняя попытка сохраняет Portfolio
- **WHEN** клиент получает полный Portfolio и `meta` с неуспехом последней часовой попытки - **WHEN** клиент получает полный Portfolio и `meta` с неуспехом последней часовой попытки
@@ -232,7 +232,7 @@ Portfolio и предложить возврат к списку счетов.
#### Scenario: Обновление после истечения срока свежести #### Scenario: Обновление после истечения срока свежести
- **WHEN** срок свежести показанного Portfolio истёк и началось фоновое обновление данных того же Brokerage Account - **WHEN** срок свежести показанного Portfolio истёк и началось фоновое обновление данных того же Brokerage Account
- **THEN** клиент сохраняет прежние валютные итоги без индикации обновления и не заменяет доступный состав и сводку skeleton-состоянием - **THEN** клиент сохраняет прежнюю итоговую сумму без индикации обновления и не заменяет доступный состав и сводку skeleton-состоянием
#### Scenario: Ошибка обновления после успешного ответа #### Scenario: Ошибка обновления после успешного ответа
- **WHEN** клиент уже показывает Client Cached Snapshot Portfolio, а обновление этих же данных завершается временной ошибкой - **WHEN** клиент уже показывает Client Cached Snapshot Portfolio, а обновление этих же данных завершается временной ошибкой
@@ -247,8 +247,8 @@ Portfolio и предложить возврат к списку счетов.
- **THEN** до ответа прежние суммы остаются видимыми, затем клиент показывает новый результат без уведомления об обновлении и без переноса пользовательского фокуса - **THEN** до ответа прежние суммы остаются видимыми, затем клиент показывает новый результат без уведомления об обновлении и без переноса пользовательского фокуса
#### Scenario: Штатный пустой Portfolio на клиенте #### Scenario: Штатный пустой Portfolio на клиенте
- **WHEN** успешно полученный полный Portfolio содержит пустые `positions` и `cashBalances` - **WHEN** успешно полученный полный Portfolio содержит пустые `positions` и без Cash Balance
- **THEN** клиент показывает существующее empty-состояние без валютных карточек с вымышленными нулевыми итогами - **THEN** клиент показывает существующее empty-состояние без вымышленной нулевой итоговой суммы
#### Scenario: Первичный Portfolio пока недоступен #### Scenario: Первичный Portfolio пока недоступен
- **WHEN** данных Portfolio нет, а первичное получение не удалось или превысило ограниченное время ожидания - **WHEN** данных Portfolio нет, а первичное получение не удалось или превысило ограниченное время ожидания
@@ -263,7 +263,7 @@ Portfolio и предложить возврат к списку счетов.
Карточки секций `STOCK` и `BOND` MUST отображаться по одной в ряду при любой ширине экрана. Карточка акции или облигации MUST показывать визуальный логотип бренда инструмента рядом с отображаемым идентификатором позиции; при отсутствии имени файла логотипа либо неудачной загрузке изображения вместо логотипа MUST отображаться первая буква отображаемого идентификатора позиции. Абсолютный доход в карточке акции или облигации 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 сохранять понятное обозначение. Карточка `STOCK` MUST показывать только `ticker` под отображаемым идентификатором позиции, если тикер доступен; при `ticker: null` она MUST сохранять текущее отображение `isin` как fallback. Карточки `BOND` и общей секции иных типов MUST сохранять отображение `isin`. Карточки общей секции иных типов MUST сохранять прежнее поведение: компактная плитка в адаптивном grid (одна колонка на узком экране, несколько колонок на широком), абсолютный доход с текстовой подписью доходности и цветовой индикацией знака, без визуального логотипа. Плитки всех секций MUST показывать отображаемый идентификатор позиции, количество, `currentValue` как главную метрику, `absoluteReturn`, `averagePurchasePrice` и долю позиции среди всех отображаемых позиций. Отсутствующие метрики MUST быть скрыты, а не отображаться заглушкой. При форматировании денежных значений в русской локали клиент MUST отображать RUB как символ `₽`.
#### Scenario: Отдельные акции и облигации #### Scenario: Отдельные акции и облигации
- **WHEN** Portfolio содержит позиции `STOCK` и `BOND` - **WHEN** Portfolio содержит позиции `STOCK` и `BOND`
@@ -287,7 +287,7 @@ Portfolio и предложить возврат к списку счетов.
#### Scenario: Карточка позиции отображает доступные метрики #### Scenario: Карточка позиции отображает доступные метрики
- **WHEN** позиция, не относящаяся к `STOCK` или `CURRENCY`, имеет значение текущей стоимости, абсолютного дохода, средней цены покупки и ISIN - **WHEN** позиция, не относящаяся к `STOCK` или `CURRENCY`, имеет значение текущей стоимости, абсолютного дохода, средней цены покупки и ISIN
- **THEN** клиент показывает идентификатор, количество, текущую стоимость, абсолютный доход, среднюю цену покупки, долю позиции среди позиций той же валюты и ISIN - **THEN** клиент показывает идентификатор, количество, текущую стоимость, абсолютный доход, среднюю цену покупки, долю позиции среди всех отображаемых позиций и ISIN
#### Scenario: Отсутствующие метрики скрыты #### Scenario: Отсутствующие метрики скрыты
- **WHEN** у позиции отсутствует значение текущей стоимости, средней цены покупки или дохода - **WHEN** у позиции отсутствует значение текущей стоимости, средней цены покупки или дохода
@@ -297,9 +297,9 @@ Portfolio и предложить возврат к списку счетов.
- **WHEN** у позиции абсолютный доход положительный или отрицательный - **WHEN** у позиции абсолютный доход положительный или отрицательный
- **THEN** клиент отображает его зелёным для положительного и красным для отрицательного, без цвета при его отсутствии - **THEN** клиент отображает его зелёным для положительного и красным для отрицательного, без цвета при его отсутствии
#### Scenario: Доля позиции в валюте #### Scenario: Доля позиции среди отображаемых позиций
- **WHEN** позиция имеет вычислимую текущую стоимость в валюте - **WHEN** позиция имеет вычислимую текущую стоимость
- **THEN** клиент отображает её долю среди всех отображаемых позиций той же валюты, не включая Cash Balance и зеркальные `CURRENCY` positions - **THEN** клиент отображает её долю среди всех отображаемых позиций, не включая Cash Balance и зеркальные `CURRENCY` positions
#### Scenario: Одноколоночные секции акций и облигаций #### Scenario: Одноколоночные секции акций и облигаций
- **WHEN** User открывает загруженный Portfolio с позициями `STOCK` или `BOND` на широком экране - **WHEN** User открывает загруженный Portfolio с позициями `STOCK` или `BOND` на широком экране
@@ -325,150 +325,65 @@ Portfolio и предложить возврат к списку счетов.
- **WHEN** User открывает загруженный Portfolio с позициями иных типов (не `STOCK` и не `BOND`) - **WHEN** User открывает загруженный Portfolio с позициями иных типов (не `STOCK` и не `BOND`)
- **THEN** клиент отображает их компактными плитками в адаптивном grid с текстовой подписью доходности и без визуального логотипа - **THEN** клиент отображает их компактными плитками в адаптивном grid с текстовой подписью доходности и без визуального логотипа
### Requirement: Компактная сводка валютных итогов Portfolio ### Requirement: Итоговая сумма Portfolio в RUB на странице счёта
На странице Brokerage Account клиент SHALL показывать каждый полученный На странице Brokerage Account клиент SHALL показывать итоговую сумму Portfolio
валютный итог Portfolio отдельной компактной карточкой. При достаточной в рублях отдельной компактной карточкой. Метка валюты и сумма MUST располагаться
ширине карточки валютная метка и сумма MUST располагаться в одной строке; в одной строке при достаточной ширине карточки; сумма MUST оставаться основной
сумма MUST оставаться основной читаемой величиной. Обычная сводка с одной читаемой величиной. Если полное значение не помещается рядом с меткой, клиент
или тремя валютами MUST занимать меньше высоты, чем прежнее представление MUST предоставлять ему полную доступную ширину карточки и переносить содержимое
с меткой над суммой при тех же значениях и ширине экрана. с увеличением высоты. Сумма MUST NOT обрезаться, заменяться многоточием или
требовать горизонтальной прокрутки.
На широком экране несколько карточек SHALL располагаться рядом при наличии Клиент MUST обозначать RUB знаком `₽` и MUST сохранять доступную идентичность
места; на узком экране MUST сохраняться одна карточка в ряду. Если полное валюты для вспомогательных технологий («Российский рубль»). Знак сам по себе MUST
значение не помещается рядом с меткой, клиент MUST предоставлять ему полную NOT быть единственным способом определения валюты.
доступную ширину карточки и переносить содержимое с увеличением высоты.
Сводка MUST NOT обрезать суммы, заменять их многоточием или требовать
горизонтальной прокрутки для чтения карточек и значений.
#### Scenario: Обычная сводка на широком экране Сумма MUST показываться без изменения знака, цифр и дробной точности. Для русской
локали клиент SHALL сохранять группировку разрядов пробелами и десятичную запятую,
не округляя значение, не отбрасывая дробные позиции и не сокращая сумму до тысяч,
миллионов или экспоненциальной записи. Требование MUST сохраняться для длинных,
отрицательных значений и отрицательных дробей с нулевой целой частью.
- **WHEN** User открывает Portfolio с итогами `125000.50` RUB, `320.00` USD Итоговая сумма MUST NOT повторно добавлять Cash Balance, MUST NOT обещать полную
и `9876.54321` EUR на широком экране с местом для трёх карточек оценку при наличии позиций без вычислимой стоимости и MUST NOT создавать нулевую
- **THEN** клиент показывает три отдельные карточки рядом, метку и полную карточку при отсутствии итоговой суммы. Отсутствие итоговой суммы само по себе
сумму в одной строке каждой карточки, с меньшей высотой сводки, чем при MUST NOT обозначать пустой Portfolio, если позиции или Cash Balance присутствуют.
прежнем размещении метки над суммой в тех же условиях
#### Scenario: Обычная сводка на узком экране #### Scenario: Обычная сумма сохраняет дробные позиции
- **WHEN** User открывает тот же Portfolio с тремя валютными итогами - **WHEN** итоговая сумма Portfolio равна `125000.50` RUB
на узком экране - **THEN** клиент показывает `125 000,50 ₽` с сохранением каждой исходной цифры и дробной позиции
- **THEN** клиент показывает три карточки по одной в ряду, сохраняя полные
читаемые суммы и меньшую общую высоту сводки, чем при прежнем размещении
метки над суммой в тех же условиях
#### Scenario: Portfolio с одной валютой
- **WHEN** Portfolio содержит один валютный итог
- **THEN** клиент показывает одну компактную карточку без пустых карточек
для других валют; обычная сумма и метка помещаются в одну строку при
достаточной ширине на широком и узком экране
#### Scenario: Длинная сумма на узком экране
- **WHEN** User просматривает итог `123456789012.345678901` RUB при ширине
экрана 320 пикселей и значение не помещается рядом с валютной меткой
- **THEN** сумма получает полную доступную ширину карточки и при необходимости
переносится; все цифры и знак валюты читаемы без обрезания, многоточия
и горизонтальной прокрутки
### Requirement: Обозначение и доступная идентичность валюты в сводке
В валютных метках и суффиксах сумм сводки клиент MUST обозначать RUB знаком
`₽`, USD знаком `$`, EUR знаком `€` вместо видимых ISO-кодов этих валют.
Каждая сумма MUST оставаться однозначно связанной со своей исходной валютой,
включая чтение вспомогательными технологиями. Для RUB, USD и EUR MUST быть
доступны названия «Российский рубль», «Доллар США» и «Евро» соответственно.
Символ или цвет сами по себе MUST NOT быть единственным доступным способом
определения валюты.
Другие полученные валюты MUST оставаться в сводке с понятным обозначением
и доступной идентичностью. Если знак отсутствует или неоднозначен, клиент
MUST сохранять возможность различить исходную валюту по её названию или
ISO-коду, в том числе визуально; неизвестное полное название MUST NOT
подменяться вымышленным. Совпадающие знаки разных валют MUST NOT приводить
к объединению карточек или неразличимому представлению их валют.
#### Scenario: Рубль, доллар США и евро показаны знаками
- **WHEN** сводка содержит RUB, USD и EUR
- **THEN** метки показывают `₽`, `$` и `€`, а суммы заканчиваются теми же
знаками; видимые RUB, USD и EUR не подменяют выбранные знаки
#### Scenario: Валютная сумма читается вспомогательной технологией
- **WHEN** User читает валютный итог сводки вспомогательной технологией
- **THEN** ему доступны полное значение и однозначная валютная идентичность,
включая «Российский рубль», «Доллар США» или «Евро» для соответствующих валют
#### Scenario: Получена другая валюта
- **WHEN** сводка содержит валюту помимо RUB, USD и EUR
- **THEN** её отдельный итог остаётся доступным с понятным обозначением
и доступным названием либо исходным ISO-кодом при неизвестном названии
#### Scenario: У разных валют совпадает знак
- **WHEN** Portfolio содержит итоги двух исходных валют с одинаковым знаком
- **THEN** сводка показывает два отдельных итога, и User может различить
их валюты визуально и вспомогательной технологией без догадки по сумме
### Requirement: Полные точные значения и смысл валютной сводки
Сводка MUST показывать полученные валютные итоги без изменения их знака,
цифр и дробной точности. Для русской локали клиент SHALL сохранять группировку
разрядов пробелами и десятичную запятую, не округляя значение, не отбрасывая
дробные позиции и не сокращая сумму до тысяч, миллионов или экспоненциальной
записи. Требование MUST сохраняться для длинных, отрицательных значений
и отрицательных дробей с нулевой целой частью.
Каждая карточка MUST представлять только полученный итог позиций и Cash
Balance своей исходной валюты. Сводка MUST NOT конвертировать или складывать
разные валюты, показывать рублёвый итог как общую стоимость Portfolio,
повторно добавлять Cash Balance либо обещать полную оценку при наличии
позиций без вычислимой стоимости. Для отсутствующего валютного итога клиент
MUST NOT создавать нулевую карточку. Отсутствие итогов само по себе MUST NOT
обозначать пустой Portfolio, если позиции или Cash Balance присутствуют.
#### Scenario: Обычные суммы сохраняют дробные позиции
- **WHEN** в сводку поступают значения `125000.50` RUB, `320.00` USD
и `9876.54321` EUR
- **THEN** клиент показывает `125 000,50 ₽`, `320,00 $` и `9 876,54321 €`
с сохранением каждой исходной цифры и дробной позиции
#### Scenario: Длинные и отрицательные значения не теряют точность #### Scenario: Длинные и отрицательные значения не теряют точность
- **WHEN** в сводку поступают `123456789012.345678901` RUB, - **WHEN** итоговая сумма равна `123456789012.345678901` RUB или `-1234567.890123` RUB
`-1234567.890123` USD и `987654321.000001` EUR - **THEN** пользователь видит полные `123 456 789 012,345678901 ₽` или `-1 234 567,890123 ₽` без округления, сокращений и потери знака на широком и узком экране
- **THEN** пользователь видит полные `123 456 789 012,345678901 ₽`,
`-1 234 567,890123 $` и `987 654 321,000001 €` без округления,
сокращений и потери отрицательного знака на широком и узком экране
#### Scenario: Отрицательная сумма меньше единицы #### Scenario: Отрицательная сумма меньше единицы
- **WHEN** полученный валютный итог равен `-0.5` USD - **WHEN** итоговая сумма равна `-0.5` RUB
- **THEN** сводка показывает `-0,5 $`, сохраняя знак и дробную величину - **THEN** сводка показывает `-0,5 ₽`, сохраняя знак и дробную величину
#### Scenario: Несколько валют и денежные остатки #### Scenario: Длинная сумма на узком экране
- **WHEN** полученные итоги RUB и USD уже включают позиции и Cash Balance - **WHEN** User просматривает итог `123456789012.345678901` RUB при ширине экрана 320 пикселей и значение не помещается рядом с меткой валюты
соответствующих валют - **THEN** сумма получает полную доступную ширину карточки и при необходимости переносится; все цифры и знак валюты читаемы без обрезания, многоточия и горизонтальной прокрутки
- **THEN** клиент показывает эти два итога без повторного добавления денег,
межвалютного итога или представления RUB как общей стоимости Portfolio #### Scenario: Итог включает позиции и Cash Balance без повторного добавления
- **WHEN** итоговая сумма уже включает позиции и Cash Balance в RUB
- **THEN** клиент показывает её одну, без повторного добавления денег
#### Scenario: Стоимость части позиций неизвестна #### Scenario: Стоимость части позиций неизвестна
- **WHEN** Portfolio содержит позиции без вычислимой стоимости и доступные - **WHEN** Portfolio содержит позиции без вычислимой стоимости
валютные итоги, сформированные без этих позиций - **THEN** сводка показывает полученную итоговую сумму, не добавляет нули за отсутствующие стоимости и не называет результат полной оценкой Portfolio
- **THEN** сводка показывает только полученные итоги, не добавляет нули
за отсутствующие стоимости и не называет результат полной оценкой Portfolio
#### Scenario: Итоги отсутствуют при непустом Portfolio #### Scenario: Итог отсутствует при непустом Portfolio
- **WHEN** `totalsByCurrency` пуст, но `positions` или `cashBalances` непусты - **WHEN** итоговая сумма отсутствует, но `positions` или Cash Balance непусты
- **THEN** клиент не создаёт валютные карточки и не заменяет доступный - **THEN** клиент не создаёт итоговую карточку и не заменяет доступный состав Portfolio состоянием пустого портфеля
состав Portfolio состоянием пустого портфеля
### Requirement: Обновление сектора и эмитента ### Requirement: Обновление сектора и эмитента
Система SHALL обновлять сектор и бренд актива позиции при первом обходе по `hourly-investment-sync`, начавшемся не ранее чем через 24 часа после последнего успешного получения; в остальных обходах значение не меняется. Пока значение не получено, получение повторяется при каждом обходе. Система SHALL обновлять сектор и бренд актива позиции при первом обходе по `hourly-investment-sync`, начавшемся не ранее чем через 24 часа после последнего успешного получения; в остальных обходах значение не меняется. Пока значение не получено, получение повторяется при каждом обходе.