docs(specs): archive improved ledger event rows

This commit is contained in:
2026-08-26 15:03:48 +03:00
parent cf6bc17611
commit 9a8687feda
6 changed files with 325 additions and 3 deletions
@@ -0,0 +1,8 @@
schema: wealth-ledger-sdd
created: 2026-08-26
goal: Сделать записи журнала операций и последние записи на обзоре счёта визуально понятнее.
affected_areas:
- frontend
- backend
- specs
- docs
@@ -0,0 +1,151 @@
## Context
Account Ledger already preserves the signed upstream payment and the operation
identifiers, but the operations response has no instrument brand metadata. The
portfolio resolves the same metadata from the existing server-side T-Bank
boundary and uses the brand file name to render an image with an initial-based
fallback. The account overview and the full ledger currently render separate,
text-heavy operation layouts; the full ledger also keeps page data and JSX in
one component instead of the finance view-model boundary.
The public OpenAPI contract is the frontend authority. T-Bank rate limiting,
authentication and upstream failure handling remain owned by the backend.
## Goals / Non-Goals
**Goals:**
- Supply nullable instrument brand metadata with each Ledger Entry without
altering its monetary or business meaning.
- Reuse one accessible, responsive presentation of a Ledger Entry in the
account overview and full ledger.
- Present the already-signed payment with a textual directional cue, a sign and
semantic positive/negative/neutral styling.
- Preserve local error, loading, empty, filter and cursor states.
**Non-Goals:**
- No new ledger categories, financial calculations, historical balance or
currency conversion.
- No changes to the T-Bank integration boundary, access rules, pagination or
query semantics.
- No logo enrichment for Future Cash Events or Portfolio Positions outside the
existing supplied metadata.
## Decisions
### Enrich Ledger Entries through a shared instrument-metadata resolver
The backend will extract the existing Portfolio instrument lookup and its
short-lived metadata caching into a reusable resolver. After obtaining a ledger
page, the service will resolve unique operation instrument identities through
that resolver and attach nullable `logoName` metadata to its entries. Lookup by
UID remains preferred; FIGI is the fallback. An unavailable or unresolvable
instrument remains a successful Ledger Entry with `logoName: null`; enrichment
must not hide a ledger page or change its money, category or identifiers.
This preserves the existing server-side T-Bank boundary and its rate limiting,
deduplicates repeated instruments in one page and avoids a separate visual-data
API. The shared resolver replaces Portfolio's private copy so the two consumers
have identical lookup and failure semantics.
### Make the additive contract explicit and nullable
`BrokerageOperationDto` gains the nullable logo file name as presentation
metadata. The generated backend OpenAPI and committed frontend snapshot remain
the only contract path. Existing consumers can ignore the additive field;
updated consumers must treat absence as normal and use the text fallback.
### Use a shared Ledger Entry presentation component
The frontend will expose a focused entity-level presentation component for a
Ledger Entry and use it both in the account overview and the full journal. It
will show the logo image when available, otherwise a deterministic initial
fallback; the visible operation and instrument text remains the primary label.
The existing image URL formatter is reused. Logo loading failure falls back in
place without moving the row.
The full ledger page is brought to the existing `container → ViewModel → view`
boundary while retaining its current query and interaction state. The ViewModel
prepares collection-level presentation data; the shared row owns only one
entry's display. This prevents duplication while keeping data fetching outside
presentation views.
### Convey money direction independently of colour
The row derives direction only from the sign already present in `payment.value`:
positive is a labelled `+` «Поступление», negative is a labelled `−` «Списание»,
and missing payment is the existing neutral «Без движения денег» state. Semantic
Mantine theme tokens supply positive, negative and neutral tones; the sign and
visible Russian label make the state understandable without colour. Amounts
remain formatted from their decimal string and are not converted to JavaScript
numbers for the direction decision.
The row uses a three-part hierarchy: instrument identity on the left, operation
and date/category as secondary metadata, and a right-aligned amount/direction
group. At narrow widths the metadata may wrap below the primary line while the
amount stays associated with its entry; the list never requires horizontal
scrolling.
## Delivery Map
| Area | Status | Architectural outcome or N/A reason |
| --- | --- | --- |
| frontend | `affected` | Generated contract update, reusable accessible Ledger Entry row, and aligned overview/full-ledger layouts through the finance VM/view boundary. |
| backend | `affected` | Shared server-side instrument metadata resolver and nullable logo metadata on each operations response. |
| specs | `affected` | Approved delta requirements become the versioned behavioural contract. |
| docs | `affected` | Account Ledger as-built documentation describes the delivered metadata and directional row semantics. |
### Contract Gate Decision
| Gate | Status | Reason |
| --- | --- | --- |
| runtime-openapi | `required` | Backend adds public response metadata and frontend consumes the generated contract. |
## Solution Assessment
The change extends an existing external T-Bank metadata lookup and introduces a
shared cache consumer, so it evaluates the available approaches.
| Option | Fit and maturity | Security and operating cost | Decision |
| --- | --- | --- | --- |
| Resolve every operation independently | Uses existing client but repeats calls for the same instrument and exposes journal latency to upstream volume. | Rate limiter prevents excess but adds queueing and avoidable upstream load. | Rejected. |
| Reuse Portfolio's private cache by duplicating it in the ledger | Works locally but creates divergent TTL, failure and fallback behaviour. | Low initial cost, growing maintenance and inconsistent cache pressure. | Rejected. |
| Extract a bounded, short-lived shared resolver around the existing client | Reuses mature authenticated boundary and gives both consumers one lookup policy. | No new credential or storage; cached public brand metadata is returned only after account access. Bounded memory and non-fatal failures limit operating risk. | Chosen. |
No third-party package or new external integration is justified: the T-Bank
client already supplies the required brand metadata and resilience controls.
## Risks / Trade-offs
- A page with many distinct instruments can still need several metadata lookups
on a cold cache. Deduplication, bounded cache lifetime and the existing
limiter trade initial completeness latency for upstream protection.
- Some past operations do not identify an instrument or have no brand asset.
The mandatory text fallback keeps these rows legible without inventing data.
- An additive nullable OpenAPI field requires atomic runtime-contract and
frontend-snapshot alignment. Until the compatible frontend is delivered,
old consumers safely ignore the field.
- Green/red alone can exclude users; sign and text are required alongside
semantic colours, and images remain supplementary to visible text.
## Migration Plan
1. Introduce the shared resolver and the nullable response metadata while
preserving the operations payload and failure behaviour.
2. Publish and sync the generated OpenAPI contract, then consume it in the
frontend presentation boundary.
3. Replace the duplicated overview and journal row rendering with the shared
responsive component and update the as-built feature documentation after
verification.
### Rollback Contract
| Field | Decision |
| --- | --- |
| Status | `required` |
| Reason | The change adds a public API response field consumed by the frontend. |
| Trigger | Runtime OpenAPI mismatch, unacceptable additional ledger latency, or a regression that hides ledger entries. |
| Actions | Revert the frontend to the prior row presentation and revert the backend response enrichment; retain the pre-change response shape when rollback is complete. |
| Data safety | No persistent data, migration or credential changes occur. In-memory metadata and page caches can expire or be discarded safely. |
| Verification | Re-run affected canonical gates, the runtime OpenAPI contract gate, and the ledger scenarios for signed, unsigned and missing-logo entries. |
@@ -0,0 +1,46 @@
## Why
Сейчас последние записи на обзоре счёта и строки полного журнала воспринимаются
как плотный текст: инструмент не имеет наглядного визуального якоря, а денежное
движение трудно быстро отличить как поступление или списание. Это замедляет
просмотр недавней активности и разрывает визуальную согласованность с уже
оформленными позициями акций и облигаций.
## What Changes
- Сделать каждую Ledger Entry в «Последних событиях» и в «Журнале операций»
компактной структурированной строкой с визуальной идентичностью инструмента,
понятным fallback при её отсутствии и читаемой группировкой основных и
второстепенных сведений.
- Явно обозначать направление денежного движения: поступления и списания
должны различаться знаком, текстовой семантикой и цветом; запись без движения
денег должна оставаться нейтральной и ясно подписанной.
- Сохранить одинаковые смысл, порядок данных и визуальную иерархию для полной
истории и пяти последних записей на обзоре счёта, с адаптацией плотности к
узкому экрану.
- Не изменять состав исполненных операций, их порядок, фильтрацию, пагинацию,
расчёт денежных значений, категории, навигацию или правила доступа к счёту.
- Не добавлять в журнал расчёт доходности, баланса после операции, валютную
конвертацию или трактовку категории операции как финансового результата.
## Capabilities
### New Capabilities
Нет.
### Modified Capabilities
- `brokerage-account-ledger`: записи журнала и блок последних записей получают
доступную presentation metadata инструмента и явно отображают направление
денежного движения без изменения факта или суммы операции.
## Impact
- Пользователь быстрее распознаёт инструмент и видит, была ли конкретная
операция поступлением, списанием или движением без денег.
- Затрагиваются отображение последних записей, полный журнал и данные,
необходимые для их визуальной идентичности.
- Документация Account Ledger после поставки будет уточнена как фактическое
состояние; публичное поведение журнала требует согласованного изменения
frontend и backend.
@@ -0,0 +1,54 @@
## MODIFIED Requirements
### Requirement: Ledger entries preserve broker operation meaning
Система MUST представлять одну upstream-операцию одной записью журнала и MUST передавать её timestamp, тип операции, категорию, описание, доступные идентификаторы инструмента, доступную presentation metadata инструмента, денежную выплату, количество, цену, НКД и комиссию. Денежные поля MUST сохранять исходную валюту и точное десятичное значение.
#### Scenario: Отображение купонной выплаты
- **WHEN** T-Bank возвращает исполненную купонную операцию с выплатой и данными инструмента
- **THEN** запись журнала определяет её как income и передаёт исходную сумму выплаты, валюту и данные инструмента
#### Scenario: Отображение комиссии сделки без синтетической записи
- **WHEN** T-Bank возвращает исполненную сделку, содержащую выплату и комиссию
- **THEN** журнал содержит одну запись сделки с комиссией в качестве её детали
#### Scenario: Отображение движения бумаг без денег
- **WHEN** T-Bank возвращает исполненную операцию, изменяющую состав инструментов, но без денежной выплаты
- **THEN** запись журнала передаёт доступное количество и не содержит искусственной денежной выплаты с нулевым значением
#### Scenario: Доступна визуальная идентичность инструмента
- **WHEN** для инструмента исполненной операции доступна presentation metadata бренда
- **THEN** запись журнала передаёт её вместе с доступными идентификаторами инструмента, не меняя остальные сведения операции
### Requirement: Account overview includes recent ledger events
Обзор счёта MUST показывать пять последних исполненных записей журнала этого счёта и действие перехода к его полному журналу. Каждая показанная запись MUST иметь ту же визуальную идентичность инструмента и семантику денежного движения, что и запись полного журнала. Неуспешный или пустой запрос последних записей MUST оставаться локальным состоянием блока последних событий и MUST NOT заменять успешно отображённый обзор Portfolio.
#### Scenario: Отображение последних событий
- **WHEN** загружается обзор счёта и его журнал содержит исполненные операции
- **THEN** обзор показывает не более пяти последних записей и ссылку на полный журнал
#### Scenario: Последние записи недоступны
- **WHEN** обзор счёта успешно загружается, но запрос последних записей завершается ошибкой
- **THEN** обзор Portfolio остаётся доступным, а блок последних событий отображает собственное состояние восстановления
#### Scenario: Последняя запись не содержит visual metadata инструмента
- **WHEN** одна из последних записей не имеет доступной presentation metadata инструмента
- **THEN** обзор показывает для неё понятный текстовый fallback и сохраняет дату, тип, инструмент и денежное изменение записи доступными для чтения
### Requirement: Full ledger presents compact rows
Страница полного журнала MUST показывать для каждой записи дату, тип операции, инструмент и денежное изменение в исходной валюте в компактной структурированной строке. При доступной presentation metadata инструмента строка MUST показывать его визуальную идентичность; при её отсутствии MUST использовать понятный текстовый fallback. Для положительной денежной выплаты строка MUST явно обозначать поступление знаком, текстовой семантикой и положительным цветом; для отрицательной — списание знаком, текстовой семантикой и отрицательным цветом. Страница MUST обозначать записи без движения денег нейтрально и MUST NOT показывать цену или количество.
#### Scenario: Отображение записи только с инструментом
- **WHEN** полный журнал отображает запись без денежной выплаты
- **THEN** страница определяет её как запись без движения денег
#### Scenario: Отображение поступления
- **WHEN** полный журнал отображает запись с положительной денежной выплатой
- **THEN** строка явно показывает поступление и его денежное изменение в исходной валюте, не полагаясь только на цвет
#### Scenario: Отображение списания
- **WHEN** полный журнал отображает запись с отрицательной денежной выплатой
- **THEN** строка явно показывает списание и его денежное изменение в исходной валюте, не полагаясь только на цвет
#### Scenario: Отсутствует visual metadata инструмента
- **WHEN** полный журнал отображает запись без доступной presentation metadata инструмента
- **THEN** строка показывает понятный текстовый fallback без подмены или сокрытия доступных данных инструмента
@@ -0,0 +1,43 @@
## 1. Backend contract and metadata delivery
- [x] 1.1 [backend] Извлечь из Portfolio общий bounded resolver metadata инструмента, применить его к уникальным идентификаторам страницы журнала и добавить nullable `logoName` в Ledger Entry без изменения исходных сумм, категорий или ошибок журнала.
- **Covers:** `brokerage-account-ledger` — `Ledger entries preserve broker operation meaning`
- **Verification:** Добавить risk-based Jest-проверки UID/FIGI lookup, дедупликации, nullable fallback и сохранения содержимого Ledger Entry; выполнить `npm run verify` в `backend`.
- [x] 1.2 [backend] Опубликовать additive nullable metadata в runtime OpenAPI и проверить совместимость существующего ответа операций.
- **Covers:** `N/A` — реализация Contract Gate Decision из утверждённого Design; самостоятельного дополнительного observable requirement не создаёт.
- **Verification:** Выполнить backend runtime preflight и подтвердить наличие nullable `logoName` в `BrokerageOperationDto` опубликованного OpenAPI до frontend sync.
## 2. Frontend ledger presentation
- [x] 2.1 [frontend] Синхронизировать committed OpenAPI snapshot после опубликованного backend contract и обновить типизированное потребление Ledger Entry.
- **Covers:** `N/A` — обязательное связывание frontend с изменённым публичным contract из Design, без нового отдельного behavior.
- **Verification:** Выполнить `npm run openapi:sync` в `frontend`, проверить generated diff на nullable `logoName` и выполнить `npm run openapi:check`.
- [x] 2.2 [frontend] Создать единое presentation отображение Ledger Entry с логотипом или текстовым fallback, русскими подписями «Поступление»/«Списание», знаком и семантическими тонами; применить его к пяти последним записям обзора и полному журналу.
- **Covers:** `brokerage-account-ledger` — `Account overview includes recent ledger events`; `brokerage-account-ledger` — `Full ledger presents compact rows`
- **Verification:** Добавить риск-ориентированные Vitest-проверки положительной, отрицательной, безденежной и безлоготипной записи в обоих потребителях; вручную проверить узкую ширину, клавиатурный фокус и читаемость без цветового восприятия.
- [x] 2.3 [frontend] Перевести полный журнал на finance boundary `container → ViewModel → view`, сохранив фильтрацию, cursor-пагинацию и все loading/error/empty состояния.
- **Covers:** `N/A` — техническая структура нужна для повторного использования строки и соблюдения утверждённого UI Pattern, не расширяя Delta Specs.
- **Verification:** Выполнить `npm run verify` в `frontend`.
## 3. Documentation and SDD delivery
- [x] 3.1 [docs] Обновить as-built Account Ledger documentation после поставки: отразить nullable visual metadata и русскую семантику поступления, списания и отсутствия движения денег без дублирования change artifacts.
- **Covers:** `N/A` — docs фиксирует только проверенное поставленное состояние, определённое Delivery Map.
- **Verification:** Выполнить `npm run verify` в `docs` и проверить локальные ссылки изменённого feature document.
- [x] 3.2 [specs] Выполнить strict validation change и Delta Specs в canonical store после завершения planning artifacts.
- **Covers:** `N/A` — specs владеет versioned SDD contract и его structural validation, а application behavior реализуют frontend и backend.
- **Verification:** Выполнить `openspec validate improve-ledger-event-rows --type change --strict --store wealth-ledger-specs`, `openspec validate --specs --strict --store wealth-ledger-specs` и `git diff --check` в `specs`.
## 4. Cross-area contract and completion
- [x] 4.1 [workspace] Выполнить required runtime OpenAPI gate после backend publication и frontend snapshot sync.
- **Covers:** `N/A` — workspace координирует Contract Gate Decision между двумя владельцами, не реализуя отдельное behavior.
- **Verification:** Выполнить `bash specs/scripts/verify-runtime-openapi.sh` из workspace root.
- [x] 4.2 [workspace] Сверить поставленное поведение с Proposal, Delta Specs и Design, затем запустить canonical dispatcher и независимый read-only review перед archive.
- **Covers:** `N/A` — workspace владеет только межрепозиторной semantic verification и review, а не реализацией требований.
- **Verification:** Выполнить `bash specs/scripts/verify-workspace-change.sh improve-ledger-event-rows` из workspace root и подтвердить отсутствие Critical/Important findings в independent review.