Merge pull request 'chore: bring develop into release sync branch (WealthLedger/main#30)' (#44) from develop into chore/30-sync-release
Conventional commits / commits (pull_request) Successful in 10s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 9s

This commit was merged in pull request #44.
This commit is contained in:
2026-10-16 07:41:05 +03:00
44 changed files with 1535 additions and 49 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ name: Conventional commits
on:
pull_request:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, edited]
concurrency:
group: specs-commits-${{ gitea.event.pull_request.number }}
+5
View File
@@ -16,8 +16,13 @@ types. CI каждого участника использует только е
| --- | --- |
| Auth/session/refresh/error contract | [User authentication](openspec/specs/user-authentication/spec.md) |
| T-Bank connection/security/cache generation | [T-Bank connection](openspec/specs/tbank-connection/spec.md) |
| Владелец синхронизации T-Bank, scheduling/priority | [T-Bank data sync](openspec/specs/tbank-data-sync/spec.md) |
| Часовой обход, результат последней попытки | [Hourly investment sync](openspec/specs/hourly-investment-sync/spec.md) |
| Счета/сводки | [Brokerage accounts](openspec/specs/brokerage-accounts/spec.md) |
| Portfolio positions/totals/stale | [Portfolio](openspec/specs/portfolio-view/spec.md) |
| История стоимости Portfolio | [Portfolio value history](openspec/specs/portfolio-value-history/spec.md) |
| Справочные сведения/поиск инструментов (online-only) | [Online instrument information](openspec/specs/online-instrument-information/spec.md) |
| Ручные дебетовые карты | [Manual bank cards](openspec/specs/manual-bank-cards/spec.md) |
| Операции/cursor/filters | [Account ledger](openspec/specs/brokerage-account-ledger/spec.md) |
| Будущие выплаты | [Upcoming events](openspec/specs/brokerage-account-upcoming-events/spec.md) |
| Доходы и периоды | [Income analytics](openspec/specs/brokerage-account-income-analytics/spec.md) |
+6 -4
View File
@@ -18,6 +18,7 @@ OpenSpec применяется после явного решения Change Ow
Для native validation из этого репозитория:
```bash
npm run check:openspec
openspec schema validate wealth-ledger-sdd --verbose
openspec schema validate wealth-ledger-sdd-v2 --verbose
openspec status --change <change> --store wealth-ledger-specs
@@ -25,10 +26,11 @@ openspec validate <change> --type change --strict --store wealth-ledger-specs
openspec validate --specs --strict --store wealth-ledger-specs
```
Версия OpenSpec CLI не закреплена отдельным файлом: при обновлении проверяйте обе
schema, затронутые changes и canonical specs, фиксируя `openspec --version` вместе
с результатами проверок. Structural validation не заменяет semantic verification
и independent read-only review перед archive.
OpenSpec CLI ставится глобально и не закрепляется в `package.json`: его транзитивная
зависимость `braces` имеет advisory без исправления в 3.x. Ожидаемая версия хранится
в `openspec/.cli-version`; `npm run check:openspec` проверяет Node из `.nvmrc`, версию
CLI и доступность store, и при расхождении выводит команду исправления. Structural
validation не заменяет semantic verification и independent read-only review перед archive.
Node задаётся собственной `.nvmrc`; после `nvm use --silent` выполните `npm ci`.
`npm run verify` проверяет diff. OpenSpec validation остаётся отдельной проверкой
+1
View File
@@ -0,0 +1 @@
1.13.1
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd-v2
created: 2026-10-14
@@ -0,0 +1,63 @@
# Design: Upcoming events date filter
## Context and Constraints
The account-events page already has a URL-backed filter with draft, Apply,
Reset, and date validation. Its view model currently exposes a `maxDate` set to
today, which blocks forecast dates; the date picker has no lower bound. The
shared `defaultPeriod()` currently subtracts six elapsed 24-hour intervals and
formats both instants as Moscow calendar dates, which can collapse to fewer
than seven dates around local midnight.
The `useUrlPeriod()` hook already supplies a default when the URL has no range,
preserves valid URL dates (including past dates), and strips invalid or reversed
range parameters. Preserve these semantics. The API already supports requested
date windows; no API or persistence changes are needed.
## Technical Goals / Non-Goals
### Goals
- Compute the exact seven-date default using Moscow calendar-date boundaries.
- Let the date picker select today and future dates only.
- Preserve valid past URL periods and existing Apply/Reset behavior.
- Keep malformed, impossible, and reversed URL periods on the default path.
### Non-Goals
- Change backend event filtering, UTC inclusivity, or saved forecast window
intersection behavior.
- Remove the page filter or turn the page into a read-only forecast list.
- Reject valid past dates at the URL/API boundary.
## Design
Use the shared calendar utility as the owner of the default range. Derive the
current Moscow calendar date, then calculate the inclusive end as six calendar
days later using calendar arithmetic rather than elapsed milliseconds. Keep
the output as `YYYY-MM-DD`, matching existing URL and API parameter formats.
In the account-events page view model, expose today's date as the picker's
minimum selectable date and remove the upper date bound. The picker constraint
is presentation-only: do not add a future/past check to `isValidPeriod()` or
`useUrlPeriod()`. That keeps valid direct URLs independent of what can be newly
selected from the UI.
Keep URL normalization and Apply/Reset ownership where they are. Invalid URL
periods continue to be removed with replacement navigation and resolve to the
same default range; valid one-sided periods retain current behavior.
## Data and State
No new state or API fields are needed. Existing URL parameters remain the
applied state; the view model retains draft state until Apply. Today's picker
minimum is refreshed when opening the calendar, consistent with the existing
today-bound refresh behavior.
## Verification
Verify the default range spans seven distinct dates at ordinary times and
around Moscow midnight, picker bounds allow future dates and block past dates,
valid past URL periods are preserved, and malformed/impossible/reversed ranges
fall back to the default. Existing history, Apply, and Reset behavior must
remain intact. Use the frontend repository's local verification commands.
@@ -0,0 +1,24 @@
# Docs Impact: Upcoming events date filter
## Decision
Update the existing canonical page `docs/domains/income/upcoming-events.md`
after implementation. It already documents the date filter and URL behavior,
so no new documentation page is needed.
## Durable Information to Promote
Clarify the user-visible default as seven calendar dates inclusive of today,
and explain that the date picker permits today and future dates while valid
past ranges remain usable when supplied through a direct URL. Invalid date
ranges continue to fall back to the default.
## Not Required
No API reference, architecture page, glossary, or ADR change is expected: the
API contract and forecast-window semantics do not change.
## Verification
After implementation, confirm the existing upcoming-events domain page matches
the shipped default range, picker bounds, and URL compatibility behavior.
@@ -0,0 +1,40 @@
# Fix upcoming events date filter
## Why
Страница «Будущие события» предназначена для просмотра прогноза, но календарь
сейчас запрещает выбрать будущие даты и разрешает выбирать прошедшие. Значение
периода по умолчанию также должно соответствовать прогнозному горизонту:
семь календарных дат, начиная с сегодняшней.
## What Changes
- Оставить URL-backed фильтр периода и действия Apply/Reset.
- По умолчанию показывать семь календарных дат включительно: от сегодняшней даты
до шестого следующего календарного дня.
- В календаре разрешить даты начиная с сегодняшней; не ограничивать доступные
даты сверху.
- Сохранять валидные периоды из URL, включая прошедшие даты. Некорректный формат,
невозможную календарную дату или обратный диапазон нормализовать к периоду по
умолчанию.
## Capabilities
- `brokerage-account-upcoming-events`
## Impact
- `frontend/`: фильтр и URL-период страницы будущих событий.
- `specs/`: уточнение пользовательского поведения периода по умолчанию и
календарных ограничений существующей capability.
Изменение сохраняет выбор владельца из frontend Issue #63: фильтр остаётся.
Оно не переводит страницу в режим read-only списка исходного прогноза.
## Success Criteria
- При первом открытии без параметров период включает сегодня и следующие шесть
календарных дат.
- Пользователь может выбирать будущие даты и не может выбрать даты до сегодня.
- Прямой URL с валидными прошедшими датами сохраняет указанный период.
- Некорректный URL-период заменяется периодом по умолчанию.
@@ -0,0 +1,37 @@
# QA Analysis: Upcoming events date filter
## Verification Scope
In scope: `brokerage-account-upcoming-events` — **User can filter upcoming
events by a forecast date range** and its four scenarios in the delta spec.
Out of scope: backend date-window behavior, event calculations, forecast
freshness, and API error mapping; these are unchanged by this frontend date
filter adjustment.
## Coverage Matrix
| Requirement / Scenario | Risk | Verification |
| --- | --- | --- |
| Default range starts today and spans seven calendar dates | Off-by-one or local-midnight error yields the wrong period | Check that the inclusive range contains today and the next six distinct Moscow calendar dates, including when opened around Moscow midnight. |
| Future dates can be selected and past dates cannot | Incorrect picker bounds preserve the reported defect or block forecast dates | Open the date picker; confirm yesterday is unavailable and today plus later dates are selectable. Apply a future range and confirm the displayed period reflects it. |
| Valid past URL range is preserved | UI restriction is accidentally enforced on deep links | Open a valid past range directly and confirm the same dates remain applied and in the URL. |
| Invalid URL range falls back to the default | Malformed or impossible dates leak into requests or remain in navigation state | Open malformed, impossible, and reversed ranges; confirm the default range is applied and invalid range parameters are removed. |
## Test Data and Boundaries
- Use a controlled current date and include a case near the Europe/Moscow
calendar-date boundary.
- Cover exactly today, the sixth following calendar date, and a later future
date in the picker.
- For URL normalization, include invalid syntax, an impossible date such as
February 30, and `from` later than `to`.
- Include a valid past range to distinguish URL compatibility from picker
restrictions.
## Risks and Limitations
The calendar is a visual interaction, so system-level verification should use
the existing browser-capable UI validation available to the frontend project.
If only component-level verification is available, it can establish URL and
range state but cannot fully prove the rendered calendar's disabled dates.
@@ -0,0 +1,41 @@
# Delta for Brokerage Account Upcoming Events
## ADDED Requirements
### Requirement: User can filter upcoming events by a forecast date range
The events page MUST retain its URL-backed date-range filter. When the URL has
no `from` or `to` parameters, the applied range MUST include the current date
and the following six calendar dates. The page MUST allow selecting today and
future dates in its date picker and MUST prevent selecting dates before today.
The date-picker restriction MUST NOT invalidate or rewrite a valid past date
range supplied directly in the URL. A malformed date, impossible calendar date,
or reversed range MUST be normalized to the default range, removing invalid
`from` and `to` parameters from the URL. Applying and resetting the filter MUST
continue to use the existing URL-backed period behavior.
#### Scenario: Default range starts today and spans seven calendar dates
- **WHEN** the user opens the events page without `from` or `to` parameters
- **THEN** the applied range starts on the current calendar date
- **AND** the inclusive end date is six calendar dates after the start date
#### Scenario: Future dates can be selected and past dates cannot
- **WHEN** the user opens the date picker
- **THEN** today and later dates are selectable
- **AND** dates before today are not selectable
#### Scenario: Valid past URL range is preserved
- **WHEN** the user opens the events page with valid `from` and `to` dates in
the past and `from` is not after `to`
- **THEN** the page applies that URL range without clamping or replacing it
#### Scenario: Invalid URL range falls back to the default
- **WHEN** the URL contains a malformed date, an impossible calendar date, or
a range whose `from` date is after its `to` date
- **THEN** the page applies the default seven-calendar-date range
- **AND** invalid `from` and `to` parameters are removed from the URL
@@ -0,0 +1,61 @@
# System Analysis: Upcoming events date filter
## Scope and ownership
This change affects the frontend events page and its existing date-range
contract. The backend API already accepts optional `from` and `to` dates and
returns events for the requested range; the canonical
`brokerage-account-upcoming-events` capability defines inclusive UTC date
bounds and behavior for invalid request ranges. No backend/API contract change
is needed.
The frontend owns picker availability, URL normalization, and the default
range. The user-approved product decision is to retain the filter (rather than
replace it with an unfiltered saved-forecast list, as considered by frontend
Issue #63).
## Observable flow
```text
URL query parameters
|
v
validate calendar dates and ordering
| valid | invalid
v v
preserve supplied range remove invalid range
| |
+------------+--------------+
v
no range -> seven dates from today
|
v
GET events with inclusive range
```
The date picker prevents new selection of dates before today, but this UI
constraint is intentionally not a validation rule for a valid URL-provided
past range. API date serialization and existing inclusive-boundary semantics
remain unchanged.
## Time-zone and calendar semantics
The existing frontend date utilities represent calendar dates in the
`Europe/Moscow` zone. The default must be computed as calendar-date arithmetic
in that same zone, not by subtracting elapsed 24-hour durations from the
current instant; elapsed-time arithmetic can yield the wrong number of distinct
dates around a local midnight or daylight offset change. The current-date
boundary used by the picker and default must be consistent with this existing
frontend calendar convention.
## Failure and compatibility behavior
- Valid past query ranges remain accepted even though the picker will not
create them.
- Malformed, impossible, or reversed ranges follow the existing URL
normalization path and resolve to the default period.
- One-sided valid ranges keep their existing supported behavior; this change
does not require both query parameters to be present.
- Apply and Reset retain their existing URL/history semantics.
- Backend `INVALID_EVENTS_PERIOD`, request parameters, and saved forecast window
intersection behavior are outside this change.
@@ -0,0 +1,11 @@
# Tasks: Upcoming events date filter
- [x] 1.1 [frontend] Implement the seven-calendar-date default and date-picker bounds while preserving URL period normalization and Apply/Reset behavior.
- **Covers:** `brokerage-account-upcoming-events` — `User can filter upcoming events by a forecast date range`.
- **Source:** Proposal **What Changes**; Specs scenarios **Default range starts today and spans seven calendar dates**, **Future dates can be selected and past dates cannot**, **Valid past URL range is preserved**, and **Invalid URL range falls back to the default**; Design **Design**.
- **Verification:** Frontend owner runs relevant existing page/calendar tests and confirms Moscow-midnight default, future/past picker bounds, valid past URL preservation, and invalid URL fallback.
- [x] 1.2 [docs] Update the canonical upcoming-events page to document the shipped default range and picker/URL behavior.
- **Covers:** `brokerage-account-upcoming-events` — `User can filter upcoming events by a forecast date range`.
- **Source:** Docs Impact **Durable Information to Promote**.
- **Verification:** Confirm `docs/domains/income/upcoming-events.md` describes seven inclusive calendar dates from today, picker restrictions, valid past URL behavior, and invalid-range fallback.
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd-v2
created: 2026-10-13
@@ -0,0 +1,208 @@
# Engineering Design
## Context and Constraints
Аналитика дохода использует выбранный период, отдельные previous/year диапазоны
и публикует их через DTO и metadata; план синхронизации также запрашивает эти
диапазоны. Frontend строит блоки сравнений из ответа и показывает исполненные
операции через `LedgerEntryRow`. Полный журнал, недавние записи и личная история
бумаги используют этот же ряд; прогноз использует `BrokerageEventCard`;
календарные карточки акции и облигации реализованы внутри соответствующих
страниц. Все уже используют `EntryCard` либо существующие Mantine primitives,
но это не один предметный компонент выплаты.
Граница реализации — утверждённые Delta Specs и
[System Analysis](system-analysis/analysis.md), включая
[целевой контракт](system-analysis/contracts/income-analytics-response.schema.json).
Доступ, деньги, полнота периода, порядок записей и формы остальных API
сохраняются. Клиент получает публичный OpenAPI artifact явно.
## Technical Goals / Non-Goals
- Исключить сравнения из чтения, синхронизации, API и UI без изменения итогов
выбранного периода.
- Один компонент для купона/дивиденда в исполненной операции, прогнозе и
графике бумаги; режим определяется источником, а не знаком или датой.
- Не преобразовывать график в личный доход и не объединять чужие деньги,
прогноз и сумму на бумагу. Не удалять `buckets` или `instruments` из API.
## Implementation Impact Map
### main
N/A: нет изменений workspace, Compose, release orchestration или конфигурации.
### frontend
Affected: страница дохода и её display model, компонент записи операций,
компонент будущего события, календарные карточки акций/облигаций, локальная
копия публичного OpenAPI и генерируемые типы. Владельцы экранов сохраняют
свои списки, фильтры и локальные состояния.
### backend
Affected: расчёт и планирование income периода, чтение операций для
income-analytics, DTO ответа и план фонового получения выбранного интервала.
Источник исполненных операций и агрегация selected остаются владельцами денег.
### qa
N/A: самостоятельного репозитория QA нет; системные проверки описывает
`qa-analysis.md` без введения несуществующей инфраструктуры.
## Decisions
### main
Изменения не требуются: координация ссылками на Issue/Change, без workspace
автоматизации ради одного API перехода.
### frontend
Один предметный `PayoutCard` разместить на уровне `entities`, доступном
страницам и компонентам других slices через public export. Он использует
существующий `EntryCard` как визуальную основу; `EntryCard` не становится
владельцем финансовой семантики. Презентационные данные передаются явным
discriminated union по происхождению `executed | forecast | schedule`:
исполненная запись содержит signed payment и точный timestamp; прогноз —
nullable projected amount, dateKind и дату; график — nullable per-security
сумму, отдельную nullable оценку и доступные даты/параметры выпуска. Ветка
графика показывает дополнительные условия купона и дивиденда без размножения
карточек. У отсутствующих значений текстовый fallback, ноль форматируется
отдельно от неизвестного. Не вводить один универсальный произвольный набор
optional props, допускающий ложную комбинацию «исполнено + оценка».
Владельцы исходных данных адаптируют типизированный ответ непосредственно на
границе использования либо через узкий mapper сущности для повторных callers:
`LedgerEntryRow` передаёт точные купоны/дивиденды в `PayoutCard`, прочие
операции оставляет на прежнем пути; `BrokerageEventCard` делает то же для
coupon/dividend, сохраняя maturity/offer; страницы графиков заменяют только
содержимое календарной карточки. Доход продолжает вызывать `LedgerEntryRow`,
сохраняя `wrap`, HTML-список и occurrence key. Личная история инструмента
сохраняет quantity/price/account details там, где они доступны. Ключи списков
и порядок определяются caller, не самим компонентом.
Удалить comparison-only display model, компонент блока, подписи и стили,
относящиеся только к сравнениям. Не удалять предупреждение неполного
selected или сохранённого snapshot. Обновить текущие fixtures и регрессии
после передачи OpenAPI; генерация типов потребителя берёт только его
локальную копию artifact.
Альтернатива с одинаковым CSS для трёх существующих компонентов отвергнута:
не обеспечивает фактическое переиспользование. Альтернатива с общим DTO
backend отвергнута: источники и денежные смыслы различны, wire-контракт
остальных API не меняется.
### backend
Сократить план income-analytics до selected: сервис запрашивает один диапазон,
не конструирует comparison totals/availability; план экранной синхронизации
не включает предыдущий год и месяц. Сохранить независимый план current-month
summary и серверный выбор точных типов купона/дивиденда. В DTO удалить
`previous`, `yearComparison`, comparison DTO при отсутствии других callers;
`dependencies` сохранить в исходной форме с единственной ролью `selected`.
Текущий shared operations range reader использует индексированные имена
resource keys для трёх периодов; после одного диапазона оставить корректную
идентичность selected и убрать устаревший индексированный список. Сохраняем
status/coverage/freshness и публикацию подтверждённого периода. Дневные/
недельные/месячные buckets остаются, так как их удаление не согласовано.
Не вводить миграцию БД: убираются чтения и поля ответа, а не сохранённые
операции или подтверждённое покрытие. Никакой пересборки прошлого дохода.
## Cross-cutting Implementation
Существующая проверка пользователя/счёта и очистка личного query cache при
смене подключения остаются на местах. Изменение количества планируемых
диапазонов не добавляет work и не меняет поведение hourly attempt для других
экранов. Существующие request ID, metadata попытки и статусы дают достаточную
диагностику; новых событий или полей логирования не требуется. Пересчёта
денег, валютной конвертации и нового кэша нет.
## Data / Storage Changes
N/A: Prisma/SQLite schema и исторические записи не меняются; выбранное
покрытие продолжает подтверждаться до публикации окончательного результата.
## Contract Implementation
Backend обновляет DTO и исполняемый generated OpenAPI. Для обоих
income-analytics маршрутов проверить отсутствие `previous`/`yearComparison`
и наличие только `selected` в dependency role при сохранении nullable `data`,
остальных полей и Problem Details. Сверить обе схемы 200 с
[target JSON Schema](system-analysis/contracts/income-analytics-response.schema.json)
семантически, а не только с TypeScript shape; существующую проверку parity
runtime OpenAPI также сохранить. Передать созданный backend artifact frontend
явно; frontend обновляет snapshot и generated types штатной локальной
процедурой, без чтения соседнего checkout в сборке. Прежний клиент,
требующий удалённых полей, несовместим.
## Developer Verification Strategy
Backend: адресные проверки планирования только выбранного интервала, чтения
income summary/analytics, точных signed сумм и недоступного selected;
проверка HTTP контракта и OpenAPI parity. Frontend: наблюдаемое отсутствие
блоков сравнений в loaded/loading/error/empty; сохранение period URL,
порядка/повторов выплат и корректные режимы общего компонента на всех
потребителях. После локальных узких проверок выполнить обязательный verify
каждого изменённого implementation repo. Это developer-owned проверки;
системная матрица отдельно в `qa-analysis.md`.
## UI/UX Package
**Applicability and scope:** changed UI; пакет полностью inline, новых
изображений и assets не требуется. Один approval Design относится к этой
ревизии и приведённому ниже точному источнику переиспользования.
**Package revision:** `1`, change `unify-income-payment-cards`.
**Screens, states and interactions:** доход — фильтр, итоги, плоский список
исполненных выплат, без сравнений; прогноз на странице событий/превью —
пометка «Прогноз», неизвестная сумма отлична от нуля, дата реестра помечена;
графики акции/облигации — «Событие по графику», разные подписи per-share/
per-bond и личной оценки. В истории операций карточка сохраняет описание,
дату и доступную identity инструмента, signed Money и детали там, где были.
Loaded, empty, loading, error и обновление старого snapshot сохраняют текущие
локальные границы экранов. Оферта, погашение, сделка и налог не получают
режим выплаты. Компонент сам не переключает состояние и не инициирует запрос.
**Responsive behavior:** однотактный проход по карточкам без горизонтального
overflow при узкой ширине и длинных значениях; labels, значения и статусы
читаются текстом и клавиатурой, цвет остаётся дополнительным признаком.
Сохраняются доступные имена региона и фокус элементов фильтра.
**Reused source and differences:** immutable locator
`wealth-ledger-specs:openspec/changes/archive/2026-09-09-simplify-income-page/design.md`
(секция «Одна карточка соответствует одной выплате») — плоский income список,
одна карточка на запись, signed Money и сохранение порядка. Дополнить
только единым предметным presentation для прогнозов/графиков с явными
подписями происхождения; не переносить прогноз внутрь income страницы.
Визуальная основа реализации — существующий `EntryCard`; его геометрия
остаётся общим стилем, а не заменой `PayoutCard`. Пакет целиком inline.
## Risks / Trade-offs
- Разные даты и null-поля трёх источников провоцируют неверные labels при
слишком широком mapper: явные варианты данных и локальные проверки экранов
важнее экономии нескольких props.
- Для календаря бумаги нет логотипа/тикера в каждой записи: владелец страницы
передаёт известную identity, не создавая новый запрос.
- Меньше загружаемых диапазонов снижает объём работы, но отсутствие сравнения
может изменить порядок фонового покрытия старых периодов; цель сохранения
другой независимой истории остаётся прежней.
## Migration / Rollout / Rollback
Сначала обновить backend DTO/логику и получить опубликованный OpenAPI, затем
передать artifact frontend и обновить UI/типизацию; согласованно поставить
две стороны breaking ответа. Не выпускать старый frontend против нового
backend как поддерживаемую комбинацию. При rollback вернуть согласованную
предыдущую пару backend/frontend и прежний OpenAPI snapshot; сохранённые
операции и покрытие не требуют data rollback. Синхронизация canonical specs
и AS-IS docs выполняется по отдельным approved tasks после реализации.
## Solution Assessment
N/A: не создаётся новая инфраструктура, интеграция, storage, кэш или система
аутентификации. Существующие `EntryCard`, owner-slices и DTO сохраняются;
сопоставление альтернатив общего UI дано в решении frontend.
@@ -0,0 +1,50 @@
# Documentation Impact
## Summary
Обновление canonical AS-IS нужно: документы сейчас прямо описывают сравнения
дохода и три разных формы карточек выплат. После поставки описание должно
отражать только выбранный период и единое представление купонов/дивидендов
с разной степенью подтверждения. Целевые страницы уже существуют; новых
разделов навигации не требуется.
## Documentation Changes
| Action | Target | Category | Resulting AS-IS | Source | Verification |
| --- | --- | --- | --- | --- | --- |
| update | `docs/domains/income/overview.md` | domain | Удалены блоки и расчёт comparisons, а также сведения о comparison coverage и процентах; income API содержит только данные выбранного периода, buckets сохраняются; плоский список фактических выплат использует общее представление купона/дивиденда. | `specs/brokerage-account-income-analytics/spec.md`; `system-analysis/analysis.md` §§ Contracts, Data and State Semantics | Сверить текст о расчёте, полноте, пустоте и карточках с двумя income API ответами и фактической страницей. |
| update | `docs/domains/portfolio/all-accounts.md` | domain | Для `all` доход учитывает доступные счета, выбранный период, source currencies и принадлежность entries без годового/предыдущего сравнения. | `specs/brokerage-account-income-analytics/spec.md`; `system-analysis/analysis.md` §§ Contracts, Security and NFR at System Boundaries | В документе нет правил year comparison; данные `all` соответствуют ответу и интерфейсу. |
| update | `docs/domains/operations/overview.md` | domain | Купоны/дивиденды в полном журнале, пяти недавних операциях и истории инструмента показаны одним предметным представлением исполненной выплаты; не-доходные операции сохраняют прежнюю семантику и детали. | `specs/brokerage-account-ledger/spec.md`; `design.md` § Decisions/frontend | Сопоставить три экрана, знак, исходную валюту, описание и дополнительную историю инструмента. |
| update | `docs/domains/income/upcoming-events.md` | domain | Полный прогноз и превью используют общее представление купона/дивиденда с пометкой прогноза, nullable Money и датой реестра для dividend; offer/maturity сохраняют смысл. | `specs/brokerage-account-upcoming-events/spec.md`; `system-analysis/analysis.md` § Data and State Semantics | Сверить оба экрана с типизированным ответом, неизвестной суммой, dateKind и отсутствием фактического дохода в прогнозе. |
| update | `docs/domains/instruments/stocks.md` | domain | Будущий и прошедший дивидендный график используют общую карточку с пометкой события по графику; сумма на акцию, оценка и дата реестра не выдаются за личное зачисление. История личных операций остаётся отдельной. | `specs/instrument-income-schedules/spec.md`; `design.md` §§ Decisions/frontend, UI/UX Package | Сверить разные деньги/даты и границу между графиком и операцией на странице акции. |
| update | `docs/domains/instruments/bonds.md` | domain | Купонный календарь и прошлые события используют общую карточку с суммой на облигацию, отдельной оценкой и известными параметрами; исполнение подтверждает только личная история. | `specs/instrument-income-schedules/spec.md`; `design.md` §§ Decisions/frontend, UI/UX Package | Сверить ближайший/прошлый/недатированный купон и неизвестную сумму на странице облигации. |
| update | `docs/architecture/http-api.md` | architecture | Зафиксировать, что оба income-analytics ответа больше не содержат данных сравнения; публичный контракт остаётся backend-generated OpenAPI, frontend получает обновлённый snapshot отдельно. Не дублировать машинную схему. | `system-analysis/analysis.md` §§ Contracts, Compatibility / Migration | Сверить описание с опубликованным OpenAPI и отсутствием comparison fields/roles. |
## Diagram Promotion
Не переносить `system-analysis/sequences/income-period.mmd` и
`system-analysis/sequences/payment-sources.mmd`: устойчивые инварианты
помещаются в тематические страницы; подробный сценарный flow полезен в
архиве change, а не как параллельная AS-IS диаграмма.
## ADR Decision
N/A: изменяется продуктовый UI и ответ income API, не вводится новое
долгоживущее архитектурное правило или интеграция. Существующий
`docs/adr/017-local-tbank-reads-and-background-sync.md` — исторический
принятый документ о старом поведении; не переписывать старое решение
задним числом. Актуальный текст о выбранном периоде должен жить в
`docs/domains/income/overview.md` и cross-cutting AS-IS при необходимости.
## Glossary / Navigation Impact
Новых терминов или маршрутов нет. Существующие значения «выплата», «прогноз»,
«операция» и «событие графика» разъясняются в тематических страницах.
`docs/overview.md` сохраняет существующую ссылку на доход.
## No-Change Rationale
Не создавать новую страницу `docs/design/`, не переносить target JSON Schema
и не менять интеграционные описания T-Bank/MOEX: данные их источников и
вычисления не изменяются. Остальные docs/ и исторические ADR не требуют
обновления.
@@ -0,0 +1,90 @@
# Proposal
## Why
Сравнения с предыдущим периодом и годом ранее не помогают пользователю решить
основную задачу страницы «Пассивный доход»: увидеть фактически полученные купоны
и дивиденды за выбранный период. Они усложняют экран и требуют получения данных
за дополнительные периоды. Купоны и дивиденды в истории операций, прогнозах и
календарях инструментов представлены разными карточками; сходный вид при этом
не обеспечивает единого представления выплаты и ясной границы между полученными
деньгами и ожидаемым событием.
## What Changes
- **BREAKING**: сравнения дохода с предыдущим периодом и годом ранее перестают
рассчитываться, загружаться и предоставляться потребителям аналитики.
- Страница «Пассивный доход» показывает выбранный период, итоги по исходным
валютам и все включённые исполненные выплаты без блоков сравнений и их состояний.
- Купоны и дивиденды во всех существующих местах показа выплат используют одно
предметное представление. Оно явно различает исполненную выплату, прогноз и
запись графика инструмента, сохраняя смысл дат и денежных величин.
## Scope
### In Scope
- Аналитика дохода одного счёта и режима «Все счета»: удаление сравнения
периодов из результата и связанных чтений, при сохранении дохода выбранного
периода и самостоятельной сводки текущего месяца.
- Карточки купонов и дивидендов в доходе, журнале и недавних операциях,
личной истории инструмента, полном списке и превью будущих событий,
календарях и истории выплат акций и облигаций.
- Обновление нормативного поведения и согласование изменённого публичного
контракта с потребителями.
### Out of Scope / Non-Goals
- Возврат графика динамики либо прогноза на страницу дохода; удаление иных
исторических данных аналитики, не относящихся к сравнению периодов.
- Изменение отбора исполненных операций, расчёта итогов, налоговой трактовки,
прогноза или правил происхождения календарных событий.
- Унификация карточек сделок, налогов, комиссий, погашений и оферт.
## Capabilities
### New Capabilities
- `specs/instrument-income-schedules/spec.md` — различимое представление
будущих и прошедших событий графиков выплат акций и облигаций, не выдаваемых
за подтверждённое личное зачисление.
### Modified Capabilities
- `specs/brokerage-account-income-analytics/spec.md` — результат без сравнений
и единое представление исполненных выплат.
- `specs/brokerage-account-ledger/spec.md` — то же представление купонов и
дивидендов в полном журнале, недавних операциях и истории инструмента.
- `specs/brokerage-account-upcoming-events/spec.md` — то же представление
прогнозных купонов и дивидендов на странице событий и в превью.
## Impact
Пользователь видит более короткую страницу дохода и узнаваемую карточку
купона или дивиденда на смежных экранах. Аналитика прекращает запрос
дополнительных периодов для сравнения, меняется публичный ответ и его
потребители. Исполненные операции, будущие события и графики инструментов
сохраняют разные источники и статус достоверности. Затронуты поведение
интерфейса, инвестиционная аналитика, публикация контракта и документация
дохода и событий.
## Success Criteria
- На странице дохода нет сравнений и сообщений об их недоступности; выбранный
период, итоги, налоговое пояснение и отдельная карточка каждой фактической
выплаты доступны как прежде.
- Чтение и синхронизация дохода не требуют периодов сравнения, а ответ
аналитики их не предоставляет. Полнота выбранного периода и итоги сохраняются.
- Купон и дивиденд на перечисленных экранах представлены одним предметным
компонентом; полученная сумма имеет исходный знак и валюту, прогноз может
не иметь суммы, а календарное событие не называется полученным доходом.
- Остальные типы операций и будущих событий не теряют своего представления.
## Assumptions / Dependencies
- Изменение публичного ответа согласуется с его потребителями при поставке;
удаление сравнений намеренно несовместимо с прежней формой ответа.
- Одна карточка может показывать данные разной природы без ложного обещания
зачисления: исполненная операция, прогноз и график остаются различимыми.
- Планирование и реализация связаны с координирующей Issue
`WealthLedger/main#33`.
@@ -0,0 +1,99 @@
# QA Analysis
## Verification Scope
### In Scope
Удаление сравнения дохода по `brokerage-account-income-analytics` и
семантика выплаты на экранах дохода, журнала, истории инструмента,
будущих событий и графиков акций/облигаций по четырём delta capabilities.
Проверяется поведение пользователя и публичного API как системы.
### Out of Scope
Пересмотр правил расчёта купонов/дивидендов брокером, налоговая оценка,
удаление buckets, новые типы событий, компонентные и внутренние тесты
реализации, несуществующий QA репозиторий.
## Coverage Matrix
| Requirement / Scenario из Delta Specs | Наблюдаемая системная проверка |
| --- | --- |
| `brokerage-account-income-analytics` — removed `Period comparisons preserve calendar and currency meaning`; `Analytics summary remains readable per currency / Динамика отсутствует для периода любой длительности` | На коротком/многолетнем периоде нет comparison UI или состояний; 200 ответ обоих income-analytics маршрутов не содержит comparison fields/roles, и чтение не инициирует дополнительные диапазоны ради сравнения. |
| `User explicitly applies an analytics period represented in the URL / Черновик периода не применён`, `Применение периода`, `Восстановление применённого периода из URL`, `Сброс периода`, `Недопустимый черновик периода` | Итоги и карточки меняются только после применения; URL/back/reset, неверные даты и доступность ввода сохраняют поведение без блоков сравнений. |
| `Realized analytics never presents an incomplete result as complete / Все страницы операций успешно получены`, `Одна из страниц операций недоступна`, `В периоде нет купонов и дивидендов` | Выбранный период с полной историей показывает итог; отсутствие или ошибка покрытия не выглядит нулём; подтверждённый пустой период отличим. |
| `Income payments remain traceable to exact Ledger Entries / Каждая выплата показана отдельной карточкой`, `Отрицательная корректировка и повторяющиеся IDs сохраняются`, `Валюта видна в сумме каждой выплаты` | Порядок и количество карточек равны серверным entries; повторы и минус в исходной валюте сохранены; нет внешних групп. |
| `Income page excludes upcoming forecasts / Данные будущих событий недоступны` | Недоступный прогноз не скрывает полный полученный доход и не возвращает прогнозный блок. |
| `brokerage-account-ledger` — `Full ledger presents compact rows / Отображение купона`, `Отображение дивиденда`, `Подпись типа не дублируется`; `Account overview includes recent ledger events / Отображение последних событий` | Полный журнал, превью и история инструмента используют одну карточку выплаченного купона/дивиденда, сохраняют signed Money, детали и fallback; другие операции сохраняют ряд. |
| `brokerage-account-upcoming-events` — `Account page presents an upcoming events list / Просмотр ближайших событий`, `Прогнозный дивиденд с неизвестной суммой`; `Account overview includes upcoming events / Отображение ближайших событий` | Та же карточка для прогноза в списке и превью, «прогноз», дата реестра и отсутствие суммы не превращаются в исполнение/ноль; offer/maturity не становятся выплатой. |
| `instrument-income-schedules / Будущий купон с известными условиями`, `Прошедший дивиденд без подтверждённой операции`, `Неизвестные условия графика` | На страницах бумаг та же карточка, но пометка графика, отдельные сумма на бумагу и оценка; прошлое событие не названо полученным, null не превращён в ноль. |
## Cross-system Verification
Сравнить оба 200 ответа income-analytics с
`system-analysis/contracts/income-analytics-response.schema.json` и
опубликованным OpenAPI; проверить отсутствие comparison ranges в запросах
при чтении и планировании income, не запрещая независимый запрос прошлого
периода пользователем. Проверить неполный/ошибочный selected, недоступный счёт,
новое подключение и поздний ответ старого без утечки дохода. Порядок и
повторение ID проверить на ответе нескольких операций; нулевая сумма и
отсутствующая сумма должны иметь разные значения. Фоновые HTTP ошибки
прогноза и календаря не должны менять исполненную выплату. Отдельных
callback и новых timeout контрактов нет.
## Risk-based Priorities
1. Точность финансового смысла и граница доступа: исправления с отрицательным
знаком, мультивалютность, «прошлый график» против факта, нет ложного нуля.
2. Breaking ответ и избыточная загрузка: оба маршрута, отсутствие comparison
fields/dependencies, синхронизация только выбранного периода.
3. Регрессии соседних экранов: предложенные выплаты, maturity/offer, trades,
текущий месяц на обзоре, back/forward и сохранённый snapshot.
## Test Data and Environment Needs
Тестовый доступный и недоступный счёт, режим `all`, завершённый и неполный
периоды; coupon/dividend одной и нескольких валют, отрицательная корректировка,
повтор operation ID, подтверждённо пустой период. Прогноз с известной/нулевой/
неизвестной суммой, dividend с датой реестра; расписание с per-security Money,
отдельной оценкой и отсутствующими датами. Управляемая доступность источника
и возможность наблюдать только показанные запросы/ресурсные metadata.
## Manual / Exploratory Scope
Пройти страницу дохода с клавиатуры и узкого viewport, проверить доступные
подписи, перенос длинных сумм и отсутствие горизонтальной прокрутки; перейти
между income, журналом, личной историей инструмента, будущими событиями и
календарём выпуска. Сравнить смысл подписи в трёх режимах при одинаковой
дате и тикере. Отдельно проверить empty/loading/error/recovery и preview.
## Automation Candidates
Black-box API: сравнение фактического schema обоих маршрутов с target,
успешный/ошибочный/пустой selected и отсутствие запросов к comparison ranges.
Сквозные journeys: переход от аккаунта через income/ledger к событию прогноза
и календарю инструмента с проверкой подписей и денег. Repository automation:
`deferred until QA repo exists`; текущее подтверждение возможно вручную и
через existing system-level HTTP/browser runners без требования нового repo.
## Observability Verification
По metadata ответа различить complete, pending, failed selected; запрос
исторического периода не должен сопровождаться comparison-only dependencies.
На отказе проверить корреляцию HTTP request ID и безопасного ответа, не
ожидая новых событий журналирования или передачи личных данных в лог.
## Regression Impact
Полный журнал, недавние операции, страница акции/облигации и их фильтры,
превью/полный прогноз, summary текущего месяца, мультивалютность и
независимые loading/error states. Затронутые публичные потребители
income-analytics требуют обновления контракта как пары backend/frontend.
## Gaps / Blockers
Блокирующих semantic gaps после утверждения Specs/System Analysis нет.
Если опубликованный backend OpenAPI не удовлетворяет target либо картинка
«получено» появляется на графике, маршрутизировать несовпадение в System
Analysis/Delta Specs до принятия реализации; внутреннюю стратегию исправления
определяют владельцы реализации.
@@ -0,0 +1,138 @@
## REMOVED Requirements
### Requirement: Period comparisons preserve calendar and currency meaning
**Reason**: Сравнения с предыдущим периодом и годом ранее больше не входят в
назначение аналитики полученного дохода и требуют избыточной загрузки истории.
**Migration**: Пользователь видит только итоги и состав выплат применённого
периода; потребители ответа аналитики больше не получают данные сравнений и
состояния их доступности. Исторический период можно по-прежнему применить
самостоятельно, но система не сопоставляет его с другим периодом автоматически.
## MODIFIED Requirements
### 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, а не из глобальной навигации.
#### Scenario: Черновик периода не применён
- **WHEN** User изменяет черновик `from` или `to`, но не применяет его
- **THEN** видимые итоги и карточки выплат остаются основанными на последнем применённом периоде
#### Scenario: Выбор дат в верхнем блоке
- **WHEN** User выбирает начало и окончание периода через календарные поля
- **THEN** выбранные даты отображаются в подписанных полях в формате `ДД.ММ.ГГГГ`, а изменение остаётся черновиком до явного применения
#### Scenario: Управление фильтрами с клавиатуры
- **WHEN** User управляет верхним блоком фильтров с клавиатуры
- **THEN** он может последовательно перейти к обоим полям дат, изменить период и выполнить применение или сброс без мыши
#### Scenario: Применение периода
- **WHEN** User явно применяет допустимый черновик периода
- **THEN** страница загружает аналитику этого периода и отражает применённые `from` и `to` в URL
#### Scenario: Недопустимый черновик периода
- **WHEN** User пытается применить черновик с отсутствующей, некорректной или будущей датой либо с `from` после `to`
- **THEN** страница показывает ошибку соответствующего поля и сохраняет последний применённый период, его URL и видимую аналитику
#### Scenario: Восстановление применённого периода из URL
- **WHEN** User открывает URL страницы с допустимыми `from` и `to`
- **THEN** страница показывает аналитику за указанный применённый период
#### Scenario: Сброс периода
- **WHEN** User сбрасывает пользовательский период
- **THEN** страница немедленно применяет период от первого дня текущего месяца по Europe/Moscow до сегодняшнего дня и синхронизирует URL
### Requirement: Realized analytics never presents an incomplete result as complete
Система MUST формировать выбранный результат, его trend buckets, instrument breakdown и drill-down только после успешного получения всех необходимых cursor pages исполненных операций выбранного периода. Ошибка любой необходимой страницы MUST завершать расчёт восстанавливаемой ошибкой и MUST NOT возвращать частичные totals, buckets или breakdown как полный результат. Успешный период без подходящих выплат MUST возвращать нулевой результат и отображаться с явным объяснением пустого состояния. Система MUST NOT загружать другие периоды ради сравнения полученного дохода с выбранным периодом.
#### Scenario: Все страницы операций успешно получены
- **WHEN** для расчёта требуется несколько cursor pages и каждая страница успешно получена
- **THEN** система формирует результат по всем подходящим Ledger Entries со всех страниц выбранного периода без расчёта сравнений с другими периодами
#### Scenario: Одна из страниц операций недоступна
- **WHEN** получение любой необходимой cursor page завершается ошибкой
- **THEN** страница показывает восстанавливаемую ошибку и не показывает частичный результат как полный
#### Scenario: В периоде нет купонов и дивидендов
- **WHEN** все необходимые операции успешно получены, но среди них нет подходящих coupon или dividend Ledger Entries
- **THEN** страница показывает нулевой результат и объясняет, что за выбранный период купоны и дивиденды не найдены
### Requirement: Income payments remain traceable to exact Ledger Entries
В разделе «Выплаты» страница MUST показывать единый список карточек точных исполненных Ledger Entries применённого периода в исходном серверном порядке entries. Каждая выплата MUST иметь собственную сразу видимую карточку с описанием, датой исполнения, доступной identity Instrument и signed Money в исходной валюте. Купоны и дивиденды MUST использовать то же представление выплаты, что и на экранах журнала, прогноза и графиков инструментов, при этом карточка MUST обозначать эту сумму именно как фактически исполненную, а не прогнозную. Страница MUST NOT добавлять внешние группировочные карточки или заголовки по Instrument либо валюте, subtotals инструмента или количество выплат инструмента. Список MUST сохранять отрицательные корректировки и повторяющиеся operation IDs как отдельные записи и MUST NOT включать записи вне аналитического итога. Backend MUST обеспечивать согласованность entries, instrumentKey, валюты и receipt count при формировании ответа из одного набора записей. Frontend SHALL доверять типизированному contract без второго runtime consistency validator и MUST NOT пересчитывать денежные итоги.
#### Scenario: Каждая выплата показана отдельной карточкой
- **WHEN** выбранный период содержит несколько включённых Ledger Entries одного или разных Instrument
- **THEN** раздел «Выплаты» сразу показывает отдельную карточку каждой записи в исходном порядке без внешних группировочных блоков, раскрытия, итогов инструмента и receipt count на UI
#### Scenario: Список воспроизводит точный состав дохода
- **WHEN** User просматривает загруженный раздел «Выплаты»
- **THEN** он видит все и только включённые coupon и dividend Ledger Entries применённого периода с датой, описанием, доступной identity и исходной суммой, обозначенной как исполненная выплата
#### Scenario: Отрицательная корректировка и повторяющиеся IDs сохраняются
- **WHEN** доход содержит отрицательную корректировку либо несколько включённых entries с одинаковым operation ID
- **THEN** каждая запись остаётся отдельной карточкой, а корректировка отображается с исходным знаком
#### Scenario: Валюта видна в сумме каждой выплаты
- **WHEN** подходящие выплаты одного Instrument представлены в нескольких исходных валютах
- **THEN** каждая карточка показывает свою Money с исходной валютой, без отдельного заголовка валютной группы или объединённого мультивалютного итога
### Requirement: Analytics summary remains readable per currency
Страница MUST обозначать фактический результат как «Получено купонами и дивидендами» и для каждой исходной валюты показывать total, coupon subtotal и dividend subtotal выбранного периода. Она MUST объяснять, что tax operations не включены, не называя фактический результат gross, net, суммой до налогов, суммой после налогов или налоговым расчётом. Значения MUST оставаться доступными как читаемый текст, не только как графика, цвет или положение. Страница MUST NOT показывать блок «Динамика», его график, переключатель валюты графика или текстовый список временных buckets независимо от длительности выбранного периода. Страница MUST NOT показывать блоки «Предыдущий период» и «Годом ранее» или сообщения о недоступности сравнений.
#### Scenario: Итоги дохода доступны как текст
- **WHEN** выбранный период содержит исполненные купонные или дивидендные выплаты
- **THEN** страница показывает читаемые total, coupon subtotal и dividend subtotal с обозначением фактически полученного дохода и объяснением исключения tax operations
#### Scenario: Итоги нескольких валют остаются разделёнными
- **WHEN** результат содержит подходящие выплаты в нескольких исходных валютах
- **THEN** страница показывает отдельные читаемые итоги каждой валюты без конвертации и общего итога разных валют
#### Scenario: Динамика отсутствует для периода любой длительности
- **WHEN** User открывает аналитику или применяет допустимый период любой длительности
- **THEN** страница показывает итоги и выплаты без динамики, сравнений периодов и состояний их доступности
### Requirement: Income page excludes upcoming forecasts
Страница «Пассивный доход» MUST NOT показывать блок «Ближайший прогноз», его суммы, список будущих выплат, состояния загрузки, пустоты или ошибки. Forecast values MUST NOT входить в realized total, trend buckets или instrument breakdown. Доступность данных Future Cash Events MUST NOT влиять на отображение успешно загруженной realized analytics. Существующая самостоятельная страница «Будущие события» и её доступность из выбранного Brokerage Account MUST сохраняться.
#### Scenario: Известные будущие выплаты не появляются на странице дохода
- **WHEN** для выбранного Brokerage Account известны будущие coupon или dividend события с суммой или без неё либо maturity или offer события
- **THEN** страница дохода не показывает блок «Ближайший прогноз» и не включает эти будущие события в фактически полученный доход
#### Scenario: Изменение исторического периода не возвращает прогноз
- **WHEN** User применяет допустимый исторический период или сбрасывает фильтры
- **THEN** страница показывает соответствующую realized analytics без блока «Ближайший прогноз»
#### Scenario: Данные будущих событий недоступны
- **WHEN** realized analytics успешно загружена, а данные Future Cash Events недоступны
- **THEN** итоги и карточки выплат остаются доступными, а страница дохода не показывает состояние ошибки или загрузки прогноза
#### Scenario: Самостоятельная страница будущих событий сохраняется
- **WHEN** User переходит из выбранного Brokerage Account на существующую страницу «Будущие события»
- **THEN** эта страница остаётся доступной с прежним поведением независимо от отсутствия прогноза на странице дохода
@@ -0,0 +1,59 @@
## MODIFIED Requirements
### Requirement: Account overview includes recent ledger events
Обзор счёта MUST показывать пять последних исполненных записей журнала этого счёта и действие перехода к его полному журналу. Каждая показанная запись 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 повторять подпись типа события во вторичной метадате записи. Знак «+» / «−» и семантический цвет MUST оставаться носителями направления и MUST NOT заменяться только текстом. Страница MUST обозначать записи без движения денег нейтрально и MUST NOT показывать цену или количество.
#### Scenario: Отображение записи только с инструментом
- **WHEN** полный журнал отображает запись без денежной выплаты
- **THEN** страница определяет её как запись без движения денег
#### Scenario: Отображение поступления
- **WHEN** полный журнал отображает запись с положительной денежной выплатой
- **THEN** строка явно показывает поступление знаком и подписью конкретного типа события («Купон», «Дивиденд» или категории) и его денежное изменение в исходной валюте, не полагаясь только на цвет
#### Scenario: Отображение списания
- **WHEN** полный журнал отображает запись с отрицательной денежной выплатой
- **THEN** строка явно показывает списание знаком и подписью конкретного типа события («Купон», «Дивиденд» или категории) и его денежное изменение в исходной валюте, не полагаясь только на цвет
#### Scenario: Отображение купона
- **WHEN** полный журнал или личная история инструмента отображает исполненную операцию с upstream-типом купона
- **THEN** запись показывает подпись «Купон» и исполненную сумму в том же представлении выплаты, что на других экранах
#### Scenario: Отображение дивиденда
- **WHEN** полный журнал или личная история инструмента отображает исполненную операцию с upstream-типом дивиденда
- **THEN** запись показывает подпись «Дивиденд» и исполненную сумму в том же представлении выплаты, что на других экранах
#### Scenario: Подпись типа не дублируется
- **WHEN** полный журнал отображает запись с типом события и вторичной метадатой
- **THEN** подпись типа события присутствует в строке один раз и не повторяется во вторичной метадате записи
#### Scenario: Отсутствует visual metadata инструмента
- **WHEN** полный журнал отображает запись без доступной presentation metadata инструмента
- **THEN** строка показывает понятный текстовый fallback без подмены или сокрытия доступных данных инструмента
@@ -0,0 +1,59 @@
## MODIFIED Requirements
### Requirement: Account page presents an upcoming events list
Страница Brokerage Account MUST предоставлять read-only действие перехода на отдельную страницу «Будущие события». Эта страница MUST показывать события сохранённого полного прогноза компактным списком без фильтров, пагинации, суммарного итога или действия ручной синхронизации, с датой, типом, инструментом и прогнозной суммой в исходной валюте, если она известна. Купоны и дивиденды MUST использовать то же представление выплаты, что и исполненные купоны, дивиденды и события графика инструмента, но MUST быть явно обозначены как прогноз, а не полученные деньги. Дата дивиденда MUST быть обозначена как дата реестра. Страница MUST показывать `calculatedAt`, исходное окно прогноза и наблюдаемые freshness, полноту, время последнего успешного прогноза, последнюю часовую попытку и доступность из `meta` понятным текстом или иконкой с доступным именем, а не только цветом. Она MUST объяснять, что прогноз рассчитан по количеству бумаг на `calculatedAt`, а показанные события дополнительно ограничены запрошенным включительным UTC-окном; прогноз указан до налогов и может быть неполным из-за доступности данных источника. Открытие страницы и повтор HTTP-чтения MUST NOT запускать личную синхронизацию.
#### Scenario: Просмотр ближайших событий
- **WHEN** User открывает «Будущие события» из обзора Brokerage Account
- **THEN** он видит отсортированный read-only список событий сохранённого 30-календарного окна и `calculatedAt`, где купоны и дивиденды представлены как прогнозные выплаты, а offer обозначен без суммы
#### Scenario: Прогнозный дивиденд с неизвестной суммой
- **WHEN** прогноз содержит дивиденд с известной датой реестра, но без суммы
- **THEN** его карточка обозначает дату как дату реестра и сумму как неизвестную, не показывает нулевую или полученную выплату
#### Scenario: Пустое состояние страницы
- **WHEN** запрос будущих событий успешно возвращает пустой список
- **THEN** страница сообщает, что в сохранённом окне прогноза событий не найдено, показывает `calculatedAt` и объясняет ограничения покрытия источника
#### Scenario: Первая загрузка либо ошибка без сохранённого прогноза
- **WHEN** запрос будущих событий возвращает `data: null`, а `meta` обозначает выполнение первой загрузки либо её неуспех
- **THEN** страница показывает доступное состояние загрузки либо ошибки без частичного списка и предоставляет при ошибке действие повторить только HTTP-чтение
#### Scenario: Ошибка страницы
- **WHEN** запрос будущих событий завершается ошибкой
- **THEN** страница не показывает частичный список и предоставляет действие повторить только HTTP-чтение
#### Scenario: Ошибка обновления при сохранённом прогнозе на странице
- **WHEN** запрос будущих событий возвращает последний полный прогноз вместе с ошибкой его обновления
- **THEN** страница показывает этот прогноз с его исходными `calculatedAt` и окном, отдельно сообщает об ошибке обновления и предоставляет действие повторить только HTTP-чтение
### Requirement: Account overview includes upcoming events
Обзор Brokerage Account MUST показывать не более пяти ближайших Future Cash Event сохранённого полного прогноза в применённом запрошенном окне, его `calculatedAt`, исходное окно, наблюдаемую freshness, полноту, последнюю часовую попытку и доступность из `meta`, а также действие перехода на read-only полную страницу «Будущие события». Купоны и дивиденды MUST быть представлены той же карточкой выплаты, что на полной странице событий и остальных экранах, с явным обозначением прогнозного статуса; погашение и оферта сохраняют своё значение. Статус MUST быть понятен текстом или иконкой с доступным именем, а не только цветом. Неуспешное обновление при сохранённом прогнозе MUST оставаться локальным состоянием блока, сохранять показанные события и их исходное окно и MUST NOT заменять успешно отображённый Portfolio. Если полного прогноза нет, неуспешное обновление MUST оставаться локальным состоянием блока без частичного списка. Обзор MUST NOT предоставлять действие ручной синхронизации; его повтор MUST выполнять только HTTP-чтение.
#### Scenario: Отображение ближайших событий
- **WHEN** обзор счёта успешно загружен и сохранённый прогноз содержит будущие события
- **THEN** обзор показывает первые пять событий в порядке даты, `calculatedAt` и действие перехода к полной странице, используя единое представление для прогнозных купонов и дивидендов
#### Scenario: Ближайшие события недоступны без сохранённого прогноза
- **WHEN** обзор счёта успешно загружен, а события возвращают `data: null` и `meta` обозначает выполнение первой загрузки либо её неуспех
- **THEN** Portfolio остаётся доступным, а блок будущих событий отображает собственное доступное состояние загрузки либо ошибки без частичного списка
#### Scenario: Ближайшие события недоступны
- **WHEN** обзор счёта успешно загружен, но запрос будущих событий завершается ошибкой
- **THEN** Portfolio остаётся доступным, а блок будущих событий отображает собственную ошибку
#### Scenario: Неуспешное обновление не скрывает сохранённый прогноз
- **WHEN** обзор счёта получает сохранённый полный прогноз вместе с ошибкой его обновления
- **THEN** Portfolio и события сохранённого окна остаются доступными, а блок отдельно обозначает ошибку и предлагает повторить только HTTP-чтение
@@ -0,0 +1,25 @@
## Purpose
Позволяет отличать опубликованные календарные сведения о купонах и дивидендах
инструмента от фактически полученных личных выплат и прогноза по позиции.
## ADDED Requirements
### Requirement: Instrument payout schedules do not imply executed income
На страницах акций и облигаций известные будущие, прошедшие и недатированные события графика дивидендов или купонов MUST использовать то же представление выплаты, что и купоны/дивиденды других экранов, но MUST обозначать их как события графика, не подтверждающие фактическое зачисление. Доступные дата реестра, дата выплаты, сумма на одну бумагу и отдельная личная оценка MUST сохранять свои подписи и смысл; неизвестная сумма или дата MUST NOT подменяться нулём или вымышленной датой. Исполненная личная операция MUST оставаться отличимой от события графика, даже когда их даты совпадают.
#### Scenario: Будущий купон с известными условиями
- **WHEN** User просматривает будущий купон облигации с известными датой выплаты, суммой на облигацию и оценкой для позиции
- **THEN** единое представление показывает купон как событие графика, сохраняет обе разные суммы и дату и не называет оценку полученными деньгами
#### Scenario: Прошедший дивиденд без подтверждённой операции
- **WHEN** User просматривает в истории акции событие дивидендного графика с прошедшей датой реестра
- **THEN** представление показывает его как событие графика с суммой на акцию, а не как исполненную выплату пользователю
#### Scenario: Неизвестные условия графика
- **WHEN** в купонном или дивидендном графике отсутствует дата выплаты либо денежное значение
- **THEN** представление сохраняет известные сведения и явно различает неизвестное значение и ноль без вымышленной даты зачисления
@@ -0,0 +1,123 @@
# System Analysis
## Requirement Coverage
| Нормативная область | Поток и контракт |
| --- | --- |
| `brokerage-account-income-analytics`: удаление `Period comparisons preserve calendar and currency meaning`, выбранный период, полнота, читаемые итоги, отсутствие прогноза | [income-period.mmd](sequences/income-period.mmd), [target schema](contracts/income-analytics-response.schema.json) |
| `brokerage-account-income-analytics`: точные карточки выплат | [payment-sources.mmd](sequences/payment-sources.mmd), существующая форма entries |
| `brokerage-account-ledger`: журнал, недавние операции и история инструмента | [payment-sources.mmd](sequences/payment-sources.mmd), существующий ответ операций |
| `brokerage-account-upcoming-events`: полный список и превью | [payment-sources.mmd](sequences/payment-sources.mmd), существующий ответ событий |
| `instrument-income-schedules`: будущее, прошлое, неопределённая дата/сумма | [payment-sources.mmd](sequences/payment-sources.mmd), существующие ответы календарей |
## Systems and Responsibilities
- Пользователь выбирает применённый период и отличает исполненные выплаты от
возможных поступлений и условий выпуска.
- Frontend хранит применённые даты в URL, показывает только финансовые значения
их собственного источника и использует один предметный компонент для
купонов/дивидендов всех трёх режимов. Он не выводит исполнение из календаря.
- Backend проверяет доступ, период и покрытие операций; формирует точные
source-currency итоги и записи выбранного периода. Для income-analytics не
запрашивает предыдущий или годовой период ради сравнения.
- Сохранённые операции T-Bank остаются источником фактических выплат; прогноз
основан на позиции, календарь бумаги — на опубликованных условиях T-Bank/MOEX.
Они не становятся взаимозаменяемыми при объединении представления.
## Interaction Flows
- [Доход выбранного периода](sequences/income-period.mmd): доступ, полнота,
успешный/пустой результат и неполное покрытие без запросов сравнения.
- [Три источника карточки выплаты](sequences/payment-sources.mmd): подтверждённая
операция, будущий прогноз и событие календаря; остальные типы сохраняют
существующее представление.
## Contracts
| Производитель → потребитель | Транспорт / операция | Целевой контракт |
| --- | --- | --- |
| Backend → frontend | `GET /api/v1/brokerage-accounts/{accountId}/income-analytics`, `GET /api/v1/brokerage-accounts/all/income-analytics`, `application/json` 200 | [JSON Schema ограничений ответа](contracts/income-analytics-response.schema.json) поверх текущего backend-generated OpenAPI: убрать `data.previous`, `data.yearComparison`, comparison roles в `meta.dependencies`; оставить выбранный период и все прочие поля и ошибки в исходном смысле. |
| Backend → frontend | Income summary, операции, события, календари акций/облигаций | Без изменений wire-формы; типизированные исходные данные адаптируются к общему представлению купона/дивиденда на границе UI. |
Целевой schema-файл описывает *изменение* ответа и не подменяет полную схему
неизменённых полей; при согласовании используется вместе с предоставленным
OpenAPI snapshot. `meta.dependencies` содержит только зависимость выбранного
периода с ролью `selected`; это не новая причина загрузки другого периода.
Существующие auth, invalid-period и resource errors не меняются. Отсутствуют
новые операции/версии и callback-протоколы. Порядок entries и повторяющиеся ID
сохраняются; повторный HTTP read не должен создавать второе зачисление.
## Data and State Semantics
- `data: null` с незавершённым покрытием выбранного периода не становится
нулевым доходом. Успешный полный период без qualifying entries — доступный
нулевой результат. Свежесть и попытка обновления относятся к выбранному
результату, не к удалённым comparison-периодам.
- Купоны/дивиденды фактического дохода — точные исполненные операции с
исходным знаком, валютой и timestamp; календарное событие с прошедшей датой
не доказывает их наличие.
- Forecast projected amount может быть `null`; для dividend `dateKind=registry`
означает дату реестра, не зачисления. График хранит per-share/per-bond Money,
опциональную оценку по позиции, известные даты и время источника отдельно от
`payment` исполненной операции. Неизвестное не становится нулём.
- Общий предметный компонент получает явный режим происхождения данных;
адаптация не пересчитывает backend totals и не меняет source identity.
## Error and Recovery Semantics
- Некорректный черновик остаётся на клиенте с ошибкой поля; серверный invalid
period и отказ доступа сохраняют действующие ответы без fallback на другой
период или чужой счёт.
- Ошибка получения страницы исполненных операций не завершает покрытие
выбранного периода и не публикует частичный итог как полный. Последний
подтверждённый результат и его исходное время остаются независимыми от
неуспеха последней попытки.
- Ошибка прогноза не скрывает уже загруженный доход; отсутствие оценки,
логотипа либо даты в календаре не создаёт ложного исполнения или нуля.
- Поздний ответ прежнего подключения не должен восстановить личные данные
нового/недоступного подключения; граница доступа сохраняется.
## Observability Semantics
- Существующие status результата, `meta` полноты/попытки и request correlation
позволяют отличить полный выбранный период, pending и failure.
- Событие синхронизации выбранного периода не включает comparison-only
dependencies; получение прошлого/годового интервала для иных самостоятельных
запросов не считается сравнением дохода.
- Новых аналитических событий или чувствительных атрибутов не требуется.
## Security and NFR at System Boundaries
- Bearer-сессия, текущее подключение и доступ к выбранному счёту проверяются
до выдачи личных операций; `all` ограничен доступными счетами.
- Даты остаются включительными календарными датами Europe/Moscow, без
максимальной длительности; валюты не конвертируются. Не вводится новый SLA.
## Compatibility / Migration
Удаление `previous` и `yearComparison` из ответа намеренно breaking для
потребителя старой формы. Backend публикует обновлённый исполняемый OpenAPI;
frontend получает его как явный artifact и обновляет локальные типы прежде,
чем интеграция начнёт опираться на новую форму. Строгая semantic compatibility
между новым OpenAPI и target schema проверяется до поставки. При поставке
backend и frontend согласуются как пара, без обещания одновременной
совместимости со старым потребителем. Существующие поля buckets/instruments,
запросы сводки/журнала/прогноза и их доступность не меняются.
## Durable Documentation Candidates
- `docs/domains/income/overview.md`: отсутствие сравнений и загрузки чужих
периодов, только полнота выбранного периода.
- `docs/domains/portfolio/all-accounts.md`,
`docs/adr/017-local-tbank-reads-and-background-sync.md`: проверить
упоминания приоритета и смысла удалённых comparisons; ADR хранить как
историческое решение, не переписывать как AS-IS.
- `docs/domains/income/upcoming-events.md`, `docs/domains/instruments/stocks.md`,
`docs/domains/instruments/bonds.md`, `docs/domains/operations/overview.md`:
общая карточка при сохранении границы между исполнением, прогнозом и графиком.
## Open Questions
Нет блокирующих вопросов в утверждённом scope. Детали визуальной композиции
общего компонента остаются за Engineering Design при сохранении нормативных
различий денежного и календарного смысла.
@@ -0,0 +1,46 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://wealthledger.local/contracts/unify-income-payment-cards/income-analytics-response.schema.json",
"title": "Целевое изменение ответа income-analytics",
"description": "Ограничения поверх неизменённых свойств текущего backend-generated OpenAPI: точный контракт остальных полей остаётся у опубликованного OpenAPI. Применяется к 200 application/json обоих маршрутов income-analytics.",
"type": "object",
"required": ["data", "meta"],
"properties": {
"data": {
"anyOf": [
{ "type": "null" },
{
"type": "object",
"required": [
"accountId",
"period",
"totalsByCurrency",
"calculatedAt",
"bucketGranularity",
"buckets",
"instruments",
"entries"
],
"properties": {
"previous": false,
"yearComparison": false
}
}
]
},
"meta": {
"type": "object",
"required": ["dependencies"],
"properties": {
"dependencies": {
"type": "array",
"items": {
"type": "object",
"required": ["role"],
"properties": { "role": { "const": "selected" } }
}
}
}
}
}
}
@@ -0,0 +1,26 @@
sequenceDiagram
actor User
participant UI as WealthLedger frontend
participant API as WealthLedger backend
participant Store as Saved investment data
participant Broker as T-Bank
User->>UI: Open income page / apply from,to
UI->>API: GET income-analytics(accountId or all, from, to)
API->>Store: Verify session, membership, selected-period coverage and operations
alt Access denied or invalid period
API-->>UI: Problem Details (access/input error)
UI-->>User: Account/period error; preserve last applied data on invalid draft
else Selected period complete
Store-->>API: Confirmed selected operations and source state
API-->>UI: data with selected totals/entries + meta(selected)
UI-->>User: Totals and executed payment cards, no comparisons
else Selected period missing or incomplete
Store-->>API: Incomplete coverage + resource state
API-->>UI: data null + meta(selected)
UI-->>User: Loading or recoverable error, no partial final totals
opt First read of missing historical interval
API->>Broker: Begin bounded acquisition of missing selected interval
Broker-->>Store: Publish only confirmed complete result
end
end
Note over API,Store: No previous/year-ago interval is requested for comparison
@@ -0,0 +1,22 @@
sequenceDiagram
actor User
participant UI as WealthLedger frontend
participant API as WealthLedger backend
participant Source as Broker / market source
alt Executed coupon or dividend
Source-->>API: Confirmed operation with signed payment and timestamp
API-->>UI: Ledger/income entry with original Money and instrument identity
UI-->>User: Shared payout card - executed, original signed amount
else Upcoming coupon or dividend
Source-->>API: Forecast event based on position at calculatedAt
API-->>UI: Future event, date kind and nullable projected Money
UI-->>User: Same payout card - forecast, registry date where applicable
else Instrument schedule event
Source-->>API: Published coupon/dividend terms, possibly undated/unknown
API-->>UI: Per-security amount, optional estimate and dates
UI-->>User: Same payout card - schedule, not a confirmed receipt
else Other operation or future event
API-->>UI: Trade/tax/fee/maturity/offer data
UI-->>User: Existing non-payout representation
end
Note over UI,User: Same payout presentation; source and certainty remain explicit
@@ -0,0 +1,67 @@
# Tasks
## 1. Удаление сравнений и поставка контракта дохода
- [x] 1.1 [backend] Оставить для дохода только выбранный период в чтении и планировании фоновой загрузки; сохранить точные итоги, signed entries, полноту, текущую месячную сводку и существующие buckets.
- **Covers:** `brokerage-account-income-analytics` — `Period comparisons preserve calendar and currency meaning` (удаление); `brokerage-account-income-analytics` — `Realized analytics never presents an incomplete result as complete`; `brokerage-account-income-analytics` — `Realized income analytics is account-scoped and period-bound`; `brokerage-account-income-analytics` — `Account overview has an independent current-month income summary`.
- **Source:** `design.md` §§ Decisions/backend, Data / Storage Changes; `system-analysis/analysis.md` §§ Interaction Flows, Data and State Semantics.
- **Verification:** адресные backend tests: один выбранный диапазон в аналитике и screen sync, без previous/year; полные, неполные, пустые, многостраничные и signed-мультивалютные результаты; неизменная current-month summary.
- **Depends on:** none
- [x] 1.2 [backend] Удалить comparison-only поля и роли из обоих публичных income-analytics ответов и опубликовать обновлённый backend-generated OpenAPI как явный контракт для frontend.
- **Covers:** `brokerage-account-income-analytics` — `Period comparisons preserve calendar and currency meaning` (удаление); `brokerage-account-income-analytics` — `Realized analytics never presents an incomplete result as complete`.
- **Source:** `system-analysis/analysis.md` §§ Contracts, Compatibility / Migration; `system-analysis/contracts/income-analytics-response.schema.json`; `design.md` § Contract Implementation.
- **Verification:** `npm run openapi:generate`, `npm run test:openapi-contract` и сопоставление 200 response schema обоих маршрутов с утверждённым target: отсутствуют `previous`, `yearComparison` и роли `previous`/`year`, остальные данные/ошибки сохранены; релевантные HTTP checks и `npm run verify` в backend.
- **Depends on:** 1.1
- [x] 1.3 [frontend] Обновить полученный OpenAPI snapshot и generated types, удалить UI, display-only расчёты и состояния сравнений на странице дохода, сохранив фильтры, итоги, налоговое пояснение, плоские выплаты и состояния выбранного периода.
- **Covers:** `brokerage-account-income-analytics` — `Period comparisons preserve calendar and currency meaning` (удаление); `brokerage-account-income-analytics` — `User explicitly applies an analytics period represented in the URL`; `brokerage-account-income-analytics` — `Analytics summary remains readable per currency`; `brokerage-account-income-analytics` — `Income page excludes upcoming forecasts`; `brokerage-account-income-analytics` — `Realized analytics never presents an incomplete result as complete`.
- **Source:** `design.md` §§ Decisions/frontend, Contract Implementation, UI/UX Package; `system-analysis/analysis.md` § Contracts.
- **Verification:** frontend behavior checks для applied/draft периода, URL/back/reset, неполного/пустого/ошибочного selected и отсутствия comparison headings/notices во всех состояниях; локальные typecheck и `npm run verify` на итоговом frontend diff.
- **Depends on:** 1.2
## 2. Одна предметная карточка купона и дивиденда
- [x] 2.1 [frontend] Предоставить единый публичный компонент выплаты с явными режимами исполнения, прогноза и графика, сохранив исходную валюту, знак, отсутствие данных и различимые подписи суммы/дат.
- **Covers:** `brokerage-account-income-analytics` — `Income payments remain traceable to exact Ledger Entries`; `brokerage-account-ledger` — `Full ledger presents compact rows`; `brokerage-account-upcoming-events` — `Account page presents an upcoming events list`; `instrument-income-schedules` — `Instrument payout schedules do not imply executed income`.
- **Source:** `design.md` §§ Decisions/frontend, UI/UX Package; `system-analysis/analysis.md` § Data and State Semantics.
- **Verification:** frontend checks разных source modes с signed Money, нулём против неизвестного, registry date и separate per-security/position amounts; `npm run deps`, `npm run typecheck`.
- **Depends on:** none
- [x] 2.2 [frontend] Использовать единый компонент для исполненных купонов/дивидендов в доходе, полном журнале, недавних операциях и личной истории инструментов; сохранить порядок, повторы и представление остальных операций.
- **Covers:** `brokerage-account-income-analytics` — `Income payments remain traceable to exact Ledger Entries`; `brokerage-account-ledger` — `Account overview includes recent ledger events`; `brokerage-account-ledger` — `Full ledger presents compact rows`.
- **Source:** `design.md` §§ Decisions/frontend, UI/UX Package; `qa-analysis.md` § Coverage Matrix.
- **Verification:** frontend checks для одного и того же типа карточки во всех перечисленных местах, повторов IDs, исправлений с минусом, fallback identity, деталей личной истории и неизменённых trade/tax rows.
- **Depends on:** 2.1
- [x] 2.3 [frontend] Использовать тот же компонент в полном прогнозе и его превью только для купонов/дивидендов, обозначить прогноз и дату реестра дивиденда, сохранить offer/maturity.
- **Covers:** `brokerage-account-upcoming-events` — `Account page presents an upcoming events list`; `brokerage-account-upcoming-events` — `Account overview includes upcoming events`.
- **Source:** `design.md` §§ Decisions/frontend, UI/UX Package; `system-analysis/analysis.md` § Data and State Semantics.
- **Verification:** frontend checks для известной, нулевой и неизвестной projected amount, registry date, preview/full list, offer/maturity и локальных ошибок источника.
- **Depends on:** 2.1
- [x] 2.4 [frontend] Использовать тот же компонент в будущих и прошедших графиках акций/облигаций с отдельными подписями условий на бумагу и оценки без приписывания личного зачисления.
- **Covers:** `instrument-income-schedules` — `Instrument payout schedules do not imply executed income`.
- **Source:** `design.md` §§ Decisions/frontend, UI/UX Package; `system-analysis/analysis.md` § Data and State Semantics.
- **Verification:** frontend checks для будущего купона, прошлого дивиденда, недатированного/неизвестного условия, разных сумм на бумагу и позицию; проверка узкого viewport и клавиатуры, `npm run verify` на итоговом frontend diff.
- **Depends on:** 2.1
## 3. Актуальная документация после подтверждения целевого поведения
- [x] 3.1 [docs] Привести страницы дохода одного и всех счетов к состоянию без сравнений и описать прежнюю полноту выбранного периода.
- **Covers:** `brokerage-account-income-analytics` — `Period comparisons preserve calendar and currency meaning` (удаление); `brokerage-account-income-analytics` — `Analytics summary remains readable per currency`; `brokerage-account-income-analytics` — `Realized analytics never presents an incomplete result as complete`.
- **Source:** `docs-impact.md` § Documentation Changes — `docs/domains/income/overview.md`, `docs/domains/portfolio/all-accounts.md`.
- **Verification:** сопоставление двух страниц с согласованными ответами income API, UI и проверка ссылок/Markdown средствами docs repo.
- **Depends on:** 1.2, 1.3
- [x] 3.2 [docs] Зафиксировать одинаковую карточку исполненной выплаты, прогнозного события и графика с разными финансовыми смыслами на существующих тематических страницах.
- **Covers:** `brokerage-account-income-analytics` — `Income payments remain traceable to exact Ledger Entries`; `brokerage-account-ledger` — `Full ledger presents compact rows`; `brokerage-account-ledger` — `Account overview includes recent ledger events`; `brokerage-account-upcoming-events` — `Account page presents an upcoming events list`; `brokerage-account-upcoming-events` — `Account overview includes upcoming events`; `instrument-income-schedules` — `Instrument payout schedules do not imply executed income`.
- **Source:** `docs-impact.md` § Documentation Changes — `docs/domains/operations/overview.md`, `docs/domains/income/upcoming-events.md`, `docs/domains/instruments/stocks.md`, `docs/domains/instruments/bonds.md`.
- **Verification:** сверить текст четырёх страниц с экранами и публичными исходными данными, проверить ссылки/Markdown средствами docs repo.
- **Depends on:** 2.2, 2.3, 2.4
- [x] 3.3 [docs] Обновить описание HTTP income analytics без дублирования машинной схемы.
- **Covers:** `brokerage-account-income-analytics` — `Period comparisons preserve calendar and currency meaning` (удаление).
- **Source:** `docs-impact.md` § Documentation Changes — `docs/architecture/http-api.md`; `system-analysis/analysis.md` § Contracts.
- **Verification:** сопоставить текст с опубликованным backend OpenAPI обоих маршрутов и target schema, проверить ссылки/Markdown средствами docs repo.
- **Depends on:** 1.2
@@ -53,30 +53,6 @@
- **WHEN** выбранный период содержит подходящие выплаты в нескольких исходных валютах
- **THEN** система возвращает отдельные итоги по каждой валюте без сконвертированного grand total
### Requirement: Period comparisons preserve calendar and currency meaning
Система MUST сопоставлять выбранный период с непосредственно предшествующим периодом. Для полного календарного месяца предыдущим периодом MUST быть предыдущий полный календарный месяц; для любого другого периода предыдущий период MUST иметь ту же включительную продолжительность и непосредственно предшествовать выбранному. Когда соответствующие календарные даты годом ранее доступны, система MUST также показывать year-over-year comparison; когда они недоступны, система MUST обозначать сравнение как недоступное, а не подменять его нулём. Каждое сравнение MUST показывать абсолютные coupon, dividend и total значения и их денежные разницы отдельно по исходным валютам и MUST NOT характеризовать их как инвестиционную эффективность.
#### Scenario: Сравнение полного календарного месяца
- **WHEN** User применяет период от первого до последнего дня календарного месяца
- **THEN** система сравнивает его с полным непосредственно предшествующим календарным месяцем
#### Scenario: Сравнение произвольного периода
- **WHEN** User применяет период, не являющийся полным календарным месяцем
- **THEN** система сравнивает его с непосредственно предшествующим включительным периодом той же продолжительности
#### Scenario: Доступно сравнение год к году
- **WHEN** для соответствующих календарных дат годом ранее доступна полная история подходящих операций
- **THEN** система показывает отдельные source-currency значения и денежные разницы относительно этого периода
#### Scenario: Годовая история недоступна
- **WHEN** полная история для соответствующих календарных дат годом ранее недоступна
- **THEN** система обозначает year-over-year comparison как недоступное и не показывает отсутствующие данные как нулевой доход
### 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, а не из глобальной навигации.
@@ -84,7 +60,7 @@
#### Scenario: Черновик периода не применён
- **WHEN** User изменяет черновик `from` или `to`, но не применяет его
- **THEN** видимые итоги, сравнения и карточки выплат остаются основанными на последнем применённом периоде
- **THEN** видимые итоги и карточки выплат остаются основанными на последнем применённом периоде
#### Scenario: Выбор дат в верхнем блоке
@@ -118,12 +94,12 @@
### Requirement: Realized analytics never presents an incomplete result as complete
Система MUST формировать выбранный результат, его comparisons, trend buckets, instrument breakdown и drill-down только после успешного получения всех необходимых cursor pages исполненных операций. Ошибка любой необходимой страницы MUST завершать расчёт восстанавливаемой ошибкой и MUST NOT возвращать частичные totals, comparisons, buckets или breakdown как полный результат. Успешный период без подходящих выплат MUST возвращать нулевой результат и отображаться с явным объяснением пустого состояния.
Система MUST формировать выбранный результат, его trend buckets, instrument breakdown и drill-down только после успешного получения всех необходимых cursor pages исполненных операций выбранного периода. Ошибка любой необходимой страницы MUST завершать расчёт восстанавливаемой ошибкой и MUST NOT возвращать частичные totals, buckets или breakdown как полный результат. Успешный период без подходящих выплат MUST возвращать нулевой результат и отображаться с явным объяснением пустого состояния. Система MUST NOT загружать другие периоды ради сравнения полученного дохода с выбранным периодом.
#### Scenario: Все страницы операций успешно получены
- **WHEN** для расчёта требуется несколько cursor pages и каждая страница успешно получена
- **THEN** система формирует результат по всем подходящим Ledger Entries со всех страниц
- **THEN** система формирует результат по всем подходящим Ledger Entries со всех страниц выбранного периода без расчёта сравнений с другими периодами
#### Scenario: Одна из страниц операций недоступна
@@ -156,7 +132,7 @@
### Requirement: Income payments remain traceable to exact Ledger Entries
В разделе «Выплаты» страница MUST показывать единый список карточек точных исполненных Ledger Entries применённого периода в исходном серверном порядке entries. Каждая выплата MUST иметь собственную сразу видимую карточку с описанием, датой, доступной identity Instrument и signed Money в исходной валюте. Страница MUST NOT добавлять внешние группировочные карточки или заголовки по Instrument либо валюте, subtotals инструмента или количество выплат инструмента. Список MUST сохранять отрицательные корректировки и повторяющиеся operation IDs как отдельные записи и MUST NOT включать записи вне аналитического итога. Backend MUST обеспечивать согласованность entries, instrumentKey, валюты и receipt count при формировании ответа из одного набора записей. Frontend SHALL доверять типизированному contract без второго runtime consistency validator и MUST NOT пересчитывать денежные итоги.
В разделе «Выплаты» страница MUST показывать единый список карточек точных исполненных Ledger Entries применённого периода в исходном серверном порядке entries. Каждая выплата MUST иметь собственную сразу видимую карточку с описанием, датой исполнения, доступной identity Instrument и signed Money в исходной валюте. Купоны и дивиденды MUST использовать то же представление выплаты, что и на экранах журнала, прогноза и графиков инструментов, при этом карточка MUST обозначать эту сумму именно как фактически исполненную, а не прогнозную. Страница MUST NOT добавлять внешние группировочные карточки или заголовки по Instrument либо валюте, subtotals инструмента или количество выплат инструмента. Список MUST сохранять отрицательные корректировки и повторяющиеся operation IDs как отдельные записи и MUST NOT включать записи вне аналитического итога. Backend MUST обеспечивать согласованность entries, instrumentKey, валюты и receipt count при формировании ответа из одного набора записей. Frontend SHALL доверять типизированному contract без второго runtime consistency validator и MUST NOT пересчитывать денежные итоги.
#### Scenario: Каждая выплата показана отдельной карточкой
@@ -166,7 +142,7 @@
#### Scenario: Список воспроизводит точный состав дохода
- **WHEN** User просматривает загруженный раздел «Выплаты»
- **THEN** он видит все и только включённые coupon и dividend Ledger Entries применённого периода с датой, описанием, доступной identity и исходной суммой
- **THEN** он видит все и только включённые coupon и dividend Ledger Entries применённого периода с датой, описанием, доступной identity и исходной суммой, обозначенной как исполненная выплата
#### Scenario: Отрицательная корректировка и повторяющиеся IDs сохраняются
@@ -180,7 +156,7 @@
### Requirement: Analytics summary remains readable per currency
Страница MUST обозначать фактический результат как «Получено купонами и дивидендами» и для каждой исходной валюты показывать total, coupon subtotal и dividend subtotal выбранного периода. Она MUST объяснять, что tax operations не включены, не называя фактический результат gross, net, суммой до налогов, суммой после налогов или налоговым расчётом. Значения MUST оставаться доступными как читаемый текст, не только как графика, цвет или положение. Страница MUST NOT показывать блок «Динамика», его график, переключатель валюты графика или текстовый список временных buckets независимо от длительности выбранного периода. Удаление блока MUST NOT изменять итоги или сравнения периодов.
Страница MUST обозначать фактический результат как «Получено купонами и дивидендами» и для каждой исходной валюты показывать total, coupon subtotal и dividend subtotal выбранного периода. Она MUST объяснять, что tax operations не включены, не называя фактический результат gross, net, суммой до налогов, суммой после налогов или налоговым расчётом. Значения MUST оставаться доступными как читаемый текст, не только как графика, цвет или положение. Страница MUST NOT показывать блок «Динамика», его график, переключатель валюты графика или текстовый список временных buckets независимо от длительности выбранного периода. Страница MUST NOT показывать блоки «Предыдущий период» и «Годом ранее» или сообщения о недоступности сравнений.
#### Scenario: Итоги дохода доступны как текст
@@ -195,11 +171,11 @@
#### Scenario: Динамика отсутствует для периода любой длительности
- **WHEN** User открывает аналитику или применяет допустимый период любой длительности
- **THEN** страница показывает итоги и существующие сравнения, но не показывает блок «Динамика», график, его переключатель валюты или текстовый список временных buckets
- **THEN** страница показывает итоги и выплаты без динамики, сравнений периодов и состояний их доступности
### Requirement: Income page excludes upcoming forecasts
Страница «Пассивный доход» MUST NOT показывать блок «Ближайший прогноз», его суммы, список будущих выплат, состояния загрузки, пустоты или ошибки. Forecast values MUST NOT входить в realized total, comparisons, trend buckets или instrument breakdown. Доступность данных Future Cash Events MUST NOT влиять на отображение успешно загруженной realized analytics. Существующая самостоятельная страница «Будущие события» и её доступность из выбранного Brokerage Account MUST сохраняться.
Страница «Пассивный доход» MUST NOT показывать блок «Ближайший прогноз», его суммы, список будущих выплат, состояния загрузки, пустоты или ошибки. Forecast values MUST NOT входить в realized total, trend buckets или instrument breakdown. Доступность данных Future Cash Events MUST NOT влиять на отображение успешно загруженной realized analytics. Существующая самостоятельная страница «Будущие события» и её доступность из выбранного Brokerage Account MUST сохраняться.
#### Scenario: Известные будущие выплаты не появляются на странице дохода
@@ -214,7 +190,7 @@
#### Scenario: Данные будущих событий недоступны
- **WHEN** realized analytics успешно загружена, а данные Future Cash Events недоступны
- **THEN** итоги, сравнения и карточки выплат остаются доступными, а страница дохода не показывает состояние ошибки или загрузки прогноза
- **THEN** итоги и карточки выплат остаются доступными, а страница дохода не показывает состояние ошибки или загрузки прогноза
#### Scenario: Самостоятельная страница будущих событий сохраняется
@@ -48,49 +48,60 @@
- **THEN** запись журнала передаёт её вместе с доступными идентификаторами инструмента, не меняя остальные сведения операции
### Requirement: Account overview includes recent ledger events
Обзор счёта MUST показывать пять последних исполненных записей журнала этого счёта и действие перехода к его полному журналу. Каждая показанная запись MUST иметь ту же визуальную идентичность инструмента и семантику денежного движения, что и запись полного журнала. Неуспешный или пустой запрос последних записей MUST оставаться локальным состоянием блока последних событий и MUST NOT заменять успешно отображённый обзор Portfolio.
Обзор счёта MUST показывать пять последних исполненных записей журнала этого счёта и действие перехода к его полному журналу. Каждая показанная запись MUST иметь ту же визуальную идентичность инструмента и семантику денежного движения, что и запись полного журнала. Исполненные купоны и дивиденды MUST иметь то же представление выплаты, что и на других экранах с купонами и дивидендами; остальные операции сохраняют представление записи журнала. Неуспешный или пустой запрос последних записей MUST оставаться локальным состоянием блока последних событий и MUST NOT заменять успешно отображённый обзор Portfolio.
#### Scenario: Отображение последних событий
- **WHEN** загружается обзор счёта и его журнал содержит исполненные операции
- **THEN** обзор показывает не более пяти последних записей и ссылку на полный журнал
- **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 NOT повторять подпись типа события во вторичной метадате записи. Знак «+» / «−» и семантический цвет MUST оставаться носителями направления и MUST NOT заменяться только текстом. Страница MUST обозначать записи без движения денег нейтрально и MUST NOT показывать цену или количество.
Страница полного журнала MUST показывать для каждой записи дату, тип операции, инструмент и денежное изменение в исходной валюте в компактной структурированной строке. При доступной presentation metadata инструмента строка MUST показывать его визуальную идентичность; при её отсутствии MUST использовать понятный текстовый fallback. Для купонных и дивидендных операций на странице полного журнала и в личной истории инструмента MUST использоваться то же представление выплаты, что в доходе, прогнозе и графиках инструментов, с обозначением исполненного движения. Для каждой записи денежное изменение в исходной валюте MUST сопровождаться подписью конкретного типа события и знаком: для купонной операции — «Купон», для дивидендной — «Дивиденд», для остальных записей — подпись их категории («Сделка», «Налог», «Комиссия», «Перевод», «Прочее»). Строка MUST NOT повторять подпись типа события во вторичной метадате записи. Знак «+» / «−» и семантический цвет MUST оставаться носителями направления и MUST NOT заменяться только текстом. Страница MUST обозначать записи без движения денег нейтрально и MUST NOT показывать цену или количество.
#### Scenario: Отображение записи только с инструментом
- **WHEN** полный журнал отображает запись без денежной выплаты
- **THEN** страница определяет её как запись без движения денег
#### Scenario: Отображение поступления
- **WHEN** полный журнал отображает запись с положительной денежной выплатой
- **THEN** строка явно показывает поступление знаком и подписью конкретного типа события («Купон», «Дивиденд» или категории) и его денежное изменение в исходной валюте, не полагаясь только на цвет
#### Scenario: Отображение списания
- **WHEN** полный журнал отображает запись с отрицательной денежной выплатой
- **THEN** строка явно показывает списание знаком и подписью конкретного типа события («Купон», «Дивиденд» или категории) и его денежное изменение в исходной валюте, не полагаясь только на цвет
#### Scenario: Отображение купона
- **WHEN** полный журнал отображает запись с upstream-типом купона
- **THEN** строка показывает подпись «Купон»
- **WHEN** полный журнал или личная история инструмента отображает исполненную операцию с upstream-типом купона
- **THEN** запись показывает подпись «Купон» и исполненную сумму в том же представлении выплаты, что на других экранах
#### Scenario: Отображение дивиденда
- **WHEN** полный журнал отображает запись с upstream-типом дивиденда
- **THEN** строка показывает подпись «Дивиденд»
- **WHEN** полный журнал или личная история инструмента отображает исполненную операцию с upstream-типом дивиденда
- **THEN** запись показывает подпись «Дивиденд» и исполненную сумму в том же представлении выплаты, что на других экранах
#### Scenario: Подпись типа не дублируется
- **WHEN** полный журнал отображает запись с типом события и вторичной метадатой
- **THEN** подпись типа события присутствует в строке один раз и не повторяется во вторичной метадате записи
#### Scenario: Отсутствует visual metadata инструмента
- **WHEN** полный журнал отображает запись без доступной presentation metadata инструмента
- **THEN** строка показывает понятный текстовый fallback без подмены или сокрытия доступных данных инструмента
@@ -84,43 +84,97 @@ Instrument в Future Cash Event MUST иметь тот же доменный с
- **THEN** система трактует ответ как отсутствие данных по инструменту и не завершает обновление ошибкой
### Requirement: Account page presents an upcoming events list
Страница Brokerage Account MUST предоставлять read-only действие перехода на отдельную страницу «Будущие события». Эта страница MUST показывать события сохранённого полного прогноза компактным списком без фильтров, пагинации, суммарного итога или действия ручной синхронизации, с датой, типом, инструментом и прогнозной суммой в исходной валюте, если она известна. Страница MUST показывать `calculatedAt`, исходное окно прогноза и наблюдаемые freshness, полноту, время последнего успешного прогноза, последнюю часовую попытку и доступность из `meta` понятным текстом или иконкой с доступным именем, а не только цветом. Она MUST объяснять, что прогноз рассчитан по количеству бумаг на `calculatedAt`, а показанные события дополнительно ограничены запрошенным включительным UTC-окном; прогноз указан до налогов и может быть неполным из-за доступности данных источника. Открытие страницы и повтор HTTP-чтения MUST NOT запускать личную синхронизацию.
Страница Brokerage Account MUST предоставлять read-only действие перехода на отдельную страницу «Будущие события». Эта страница MUST показывать события сохранённого полного прогноза компактным списком без фильтров, пагинации, суммарного итога или действия ручной синхронизации, с датой, типом, инструментом и прогнозной суммой в исходной валюте, если она известна. Купоны и дивиденды MUST использовать то же представление выплаты, что и исполненные купоны, дивиденды и события графика инструмента, но MUST быть явно обозначены как прогноз, а не полученные деньги. Дата дивиденда MUST быть обозначена как дата реестра. Страница MUST показывать `calculatedAt`, исходное окно прогноза и наблюдаемые freshness, полноту, время последнего успешного прогноза, последнюю часовую попытку и доступность из `meta` понятным текстом или иконкой с доступным именем, а не только цветом. Она MUST объяснять, что прогноз рассчитан по количеству бумаг на `calculatedAt`, а показанные события дополнительно ограничены запрошенным включительным UTC-окном; прогноз указан до налогов и может быть неполным из-за доступности данных источника. Открытие страницы и повтор HTTP-чтения MUST NOT запускать личную синхронизацию.
#### Scenario: Просмотр ближайших событий
- **WHEN** User открывает «Будущие события» из обзора Brokerage Account
- **THEN** он видит отсортированный read-only список событий сохранённого 30-календарного окна и `calculatedAt`, где offer обозначен без суммы
- **THEN** он видит отсортированный read-only список событий сохранённого 30-календарного окна и `calculatedAt`, где купоны и дивиденды представлены как прогнозные выплаты, а offer обозначен без суммы
#### Scenario: Прогнозный дивиденд с неизвестной суммой
- **WHEN** прогноз содержит дивиденд с известной датой реестра, но без суммы
- **THEN** его карточка обозначает дату как дату реестра и сумму как неизвестную, не показывает нулевую или полученную выплату
#### Scenario: Пустое состояние страницы
- **WHEN** запрос будущих событий успешно возвращает пустой список
- **THEN** страница сообщает, что в сохранённом окне прогноза событий не найдено, показывает `calculatedAt` и объясняет ограничения покрытия источника
#### Scenario: Первая загрузка либо ошибка без сохранённого прогноза
- **WHEN** запрос будущих событий возвращает `data: null`, а `meta` обозначает выполнение первой загрузки либо её неуспех
- **THEN** страница показывает доступное состояние загрузки либо ошибки без частичного списка и предоставляет при ошибке действие повторить только HTTP-чтение
#### Scenario: Ошибка страницы
- **WHEN** запрос будущих событий завершается ошибкой
- **THEN** страница не показывает частичный список и предоставляет действие повторить только HTTP-чтение
#### Scenario: Ошибка обновления при сохранённом прогнозе на странице
- **WHEN** запрос будущих событий возвращает последний полный прогноз вместе с ошибкой его обновления
- **THEN** страница показывает этот прогноз с его исходными `calculatedAt` и окном, отдельно сообщает об ошибке обновления и предоставляет действие повторить только HTTP-чтение
### Requirement: Account overview includes upcoming events
Обзор Brokerage Account MUST показывать не более пяти ближайших Future Cash Event сохранённого полного прогноза в применённом запрошенном окне, его `calculatedAt`, исходное окно, наблюдаемую freshness, полноту, последнюю часовую попытку и доступность из `meta`, а также действие перехода на read-only полную страницу «Будущие события». Статус MUST быть понятен текстом или иконкой с доступным именем, а не только цветом. Неуспешное обновление при сохранённом прогнозе MUST оставаться локальным состоянием блока, сохранять показанные события и их исходное окно и MUST NOT заменять успешно отображённый Portfolio. Если полного прогноза нет, неуспешное обновление MUST оставаться локальным состоянием блока без частичного списка. Обзор MUST NOT предоставлять действие ручной синхронизации; его повтор MUST выполнять только HTTP-чтение.
Обзор Brokerage Account MUST показывать не более пяти ближайших Future Cash Event сохранённого полного прогноза в применённом запрошенном окне, его `calculatedAt`, исходное окно, наблюдаемую freshness, полноту, последнюю часовую попытку и доступность из `meta`, а также действие перехода на read-only полную страницу «Будущие события». Купоны и дивиденды MUST быть представлены той же карточкой выплаты, что на полной странице событий и остальных экранах, с явным обозначением прогнозного статуса; погашение и оферта сохраняют своё значение. Статус MUST быть понятен текстом или иконкой с доступным именем, а не только цветом. Неуспешное обновление при сохранённом прогнозе MUST оставаться локальным состоянием блока, сохранять показанные события и их исходное окно и MUST NOT заменять успешно отображённый Portfolio. Если полного прогноза нет, неуспешное обновление MUST оставаться локальным состоянием блока без частичного списка. Обзор MUST NOT предоставлять действие ручной синхронизации; его повтор MUST выполнять только HTTP-чтение.
#### Scenario: Отображение ближайших событий
- **WHEN** обзор счёта успешно загружен и сохранённый прогноз содержит будущие события
- **THEN** обзор показывает первые пять событий в порядке даты, `calculatedAt` и действие перехода к полной странице
- **THEN** обзор показывает первые пять событий в порядке даты, `calculatedAt` и действие перехода к полной странице, используя единое представление для прогнозных купонов и дивидендов
#### Scenario: Ближайшие события недоступны без сохранённого прогноза
- **WHEN** обзор счёта успешно загружен, а события возвращают `data: null` и `meta` обозначает выполнение первой загрузки либо её неуспех
- **THEN** Portfolio остаётся доступным, а блок будущих событий отображает собственное доступное состояние загрузки либо ошибки без частичного списка
#### Scenario: Ближайшие события недоступны
- **WHEN** обзор счёта успешно загружен, но запрос будущих событий завершается ошибкой
- **THEN** Portfolio остаётся доступным, а блок будущих событий отображает собственную ошибку
#### Scenario: Неуспешное обновление не скрывает сохранённый прогноз
- **WHEN** обзор счёта получает сохранённый полный прогноз вместе с ошибкой его обновления
- **THEN** Portfolio и события сохранённого окна остаются доступными, а блок отдельно обозначает ошибку и предлагает повторить только HTTP-чтение
### Requirement: User can filter upcoming events by a forecast date range
The events page MUST retain its URL-backed date-range filter. When the URL has
no `from` or `to` parameters, the applied range MUST include the current date
and the following six calendar dates. The page MUST allow selecting today and
future dates in its date picker and MUST prevent selecting dates before today.
The date-picker restriction MUST NOT invalidate or rewrite a valid past date
range supplied directly in the URL. A malformed date, impossible calendar date,
or reversed range MUST be normalized to the default range, removing invalid
`from` and `to` parameters from the URL. Applying and resetting the filter MUST
continue to use the existing URL-backed period behavior.
#### Scenario: Default range starts today and spans seven calendar dates
- **WHEN** the user opens the events page without `from` or `to` parameters
- **THEN** the applied range starts on the current calendar date
- **AND** the inclusive end date is six calendar dates after the start date
#### Scenario: Future dates can be selected and past dates cannot
- **WHEN** the user opens the date picker
- **THEN** today and later dates are selectable
- **AND** dates before today are not selectable
#### Scenario: Valid past URL range is preserved
- **WHEN** the user opens the events page with valid `from` and `to` dates in
the past and `from` is not after `to`
- **THEN** the page applies that URL range without clamping or replacing it
#### Scenario: Invalid URL range falls back to the default
- **WHEN** the URL contains a malformed date, an impossible calendar date, or
a range whose `from` date is after its `to` date
- **THEN** the page applies the default seven-calendar-date range
- **AND** invalid `from` and `to` parameters are removed from the URL
@@ -0,0 +1,26 @@
# instrument-income-schedules Specification
## Purpose
Позволяет отличать опубликованные календарные сведения о купонах и дивидендах
инструмента от фактически полученных личных выплат и прогноза по позиции.
## Requirements
### Requirement: Instrument payout schedules do not imply executed income
На страницах акций и облигаций известные будущие, прошедшие и недатированные события графика дивидендов или купонов MUST использовать то же представление выплаты, что и купоны/дивиденды других экранов, но MUST обозначать их как события графика, не подтверждающие фактическое зачисление. Доступные дата реестра, дата выплаты, сумма на одну бумагу и отдельная личная оценка MUST сохранять свои подписи и смысл; неизвестная сумма или дата MUST NOT подменяться нулём или вымышленной датой. Исполненная личная операция MUST оставаться отличимой от события графика, даже когда их даты совпадают.
#### Scenario: Будущий купон с известными условиями
- **WHEN** User просматривает будущий купон облигации с известными датой выплаты, суммой на облигацию и оценкой для позиции
- **THEN** единое представление показывает купон как событие графика, сохраняет обе разные суммы и дату и не называет оценку полученными деньгами
#### Scenario: Прошедший дивиденд без подтверждённой операции
- **WHEN** User просматривает в истории акции событие дивидендного графика с прошедшей датой реестра
- **THEN** представление показывает его как событие графика с суммой на акцию, а не как исполненную выплату пользователю
#### Scenario: Неизвестные условия графика
- **WHEN** в купонном или дивидендном графике отсутствует дата выплаты либо денежное значение
- **THEN** представление сохраняет известные сведения и явно различает неизвестное значение и ноль без вымышленной даты зачисления
@@ -2,6 +2,7 @@
## Purpose
Определяет единственный источник полномочий для OpenSpec и проверяемый жизненный цикл SDD-изменений независимо от текущей директории, глобальной конфигурации или agent framework.
Применяется только к работе, которую Change Owner явно начал как OpenSpec/SDD; обычная разработка идёт по задаче без planning artifacts.
## Requirements
+77
View File
@@ -0,0 +1,77 @@
# stock-screener Specification
## Purpose
Определяет поиск доступных к покупке акций по характеристикам всего доступного каталога, просмотр совпавших выпусков и навигацию по результату поиска.
## Requirements
### Requirement: Скринер ищет среди доступных к покупке акций
Система SHALL по нажатию «Найти» искать по всему доступному на момент запроса каталогу акций, не ограничивая исходный набор первыми совпадениями текстового поиска. В результаты MUST включаться только акции, для которых в полученных сведениях подтверждены и доступность покупки, и возможность торговли через API. Скринер MUST показывать время получения списка и MUST NOT представлять эти признаки как гарантию, что конкретный пользователь сможет исполнить заявку прямо сейчас.
#### Scenario: Поиск без названия или тикера
- **WHEN** пользователь не ввёл текст и нажал «Найти»
- **THEN** система ищет по доступному каталогу и показывает выпуски, удовлетворяющие условиям и обоим признакам доступности
#### Scenario: Акция недоступна для покупки
- **WHEN** у акции отсутствует подтверждение доступности покупки или торговли через API
- **THEN** система не включает её в результаты скринера независимо от остальных совпавших условий
### Requirement: Условия скринера применяются совместно
Система SHALL позволять независимо задавать необязательные условия по сектору, типу акции, валюте расчётов, площадке, стране риска и максимальному количеству бумаг в лоте, а также необязательный текстовый запрос по названию или тикеру. После нажатия «Найти» система SHALL возвращать только акции, удовлетворяющие всем заданным условиям. Отсутствующее или неизвестное значение выбранного условия MUST NOT считаться совпадением.
#### Scenario: Несколько условий без текста
- **WHEN** пользователь выбрал сектор и валюту, оставил текст пустым и нажал «Найти»
- **THEN** в результате остаются только доступные к покупке акции выбранного сектора и валюты
#### Scenario: Текст совместно с характеристиками
- **WHEN** пользователь ввёл название или тикер, выбрал тип акции и нажал «Найти»
- **THEN** система отбирает совпадения по тексту и типу среди доступного каталога до разделения результата на страницы
#### Scenario: Неизвестный сектор
- **WHEN** пользователь выбрал сектор, а у акции сектор не указан
- **THEN** акция не считается совпадением
#### Scenario: Граница максимального лота
- **WHEN** задано максимальное количество бумаг в лоте
- **THEN** акции с известным размером лота не больше указанного включаются при соблюдении остальных условий, а акции с неизвестным или большим лотом исключаются
### Requirement: Результат представляет отдельные выпуски
Система SHALL показывать не более одной карточки для каждого выпуска с одинаковым ISIN; акции с разными ISIN SHALL оставаться отдельными результатами. При отсутствии ISIN отдельным результатом SHALL быть инструмент с собственным идентификатором. Представитель выпуска MUST выбираться среди инструментов, удовлетворяющих заданным условиям, а карточка MUST вести к выбранному инструменту.
#### Scenario: Несколько подходящих режимов одного выпуска
- **WHEN** несколько инструментов с одним ISIN удовлетворяют всем условиям
- **THEN** система показывает одну карточку выпуска со ссылкой на один из этих инструментов
#### Scenario: Один режим не соответствует условиям
- **WHEN** два инструмента имеют один ISIN, но только один удовлетворяет всем условиям
- **THEN** карточка выпуска ведёт к соответствующему условиям инструменту
#### Scenario: Разные выпуски одного эмитента
- **WHEN** обыкновенная и привилегированная акции имеют разные ISIN и обе удовлетворяют условиям
- **THEN** система показывает их как отдельные результаты
### Requirement: Страницы относятся к одному результату поиска
Система SHALL упорядочивать совпадения по тикеру, затем по идентификатору инструмента, и показывать не более 30 выпусков на странице. Переход между страницами одного поиска MUST сохранять состав и порядок полученного результата без пропусков и повторов. Изменение полей без нажатия «Найти» MUST NOT заменять текущий результат; повторное нажатие «Найти», в том числе без изменения условий, SHALL запускать новый поиск и возвращать к первой странице.
#### Scenario: Переход к следующей странице
- **WHEN** пользователь открывает следующую страницу полученного результата
- **THEN** он видит следующие до 30 выпусков в том же порядке и с тем же временем получения списка, без повторения выпусков предыдущей страницы
#### Scenario: Повторный поиск
- **WHEN** пользователь изменил условия или оставил их прежними и нажал «Найти» повторно
- **THEN** система получает новый результат с новым временем получения списка и показывает его первую страницу
#### Scenario: Поля изменены, но поиск не запущен
- **WHEN** пользователь изменяет условия и ещё не нажал «Найти»
- **THEN** ранее полученные страницы не подменяются результатом для новых условий
### Requirement: Отсутствие совпадений отличается от недоступности поиска
Система MUST показывать пустой результат только после успешного поиска без совпадений. Если получить сведения для поиска не удалось, система SHALL сообщать о недоступности поиска и MUST NOT выдавать отсутствие акций за подтверждённый результат.
#### Scenario: Успешный поиск без совпадений
- **WHEN** поиск завершился успешно, но ни одна акция не удовлетворяет условиям
- **THEN** система показывает, что подходящие акции не найдены
#### Scenario: Источник каталога недоступен
- **WHEN** поиск не может получить сведения о каталоге
- **THEN** система сообщает об ошибке получения результата, не утверждая, что подходящих акций нет
+1
View File
@@ -7,6 +7,7 @@
},
"scripts": {
"verify": "git diff --check",
"check:openspec": "node scripts/check-openspec.cjs",
"release:preview": "release-it --dry-run --ci",
"release:prepare": "release-it --ci",
"commitlint": "commitlint",
+39
View File
@@ -0,0 +1,39 @@
const assert = require("node:assert/strict");
const { execFileSync } = require("node:child_process");
const { readFileSync } = require("node:fs");
const path = require("node:path");
// OpenSpec CLI is installed globally (no npm pin: its transitive braces advisory has no 3.x fix).
// This check fails fast with an actionable message instead of silently using another version.
const root = path.resolve(__dirname, "..");
const expectedCli = readFileSync(path.join(root, "openspec/.cli-version"), "utf8").trim();
const expectedNode = readFileSync(path.join(root, ".nvmrc"), "utf8").trim();
const store = "wealth-ledger-specs";
const run = (command, args) =>
execFileSync(command, args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
assert.equal(
process.version.replace(/^v/, ""), expectedNode,
`Node ${process.version} differs from .nvmrc ${expectedNode}. Run: nvm use --silent`,
);
let cliVersion;
try {
cliVersion = run("openspec", ["--version"]);
} catch {
assert.fail(`openspec CLI not found. Install it globally: npm install -g @fission-ai/openspec@${expectedCli}`);
}
assert.equal(
cliVersion, expectedCli,
`openspec ${cliVersion} differs from openspec/.cli-version ${expectedCli}. Reinstall: npm install -g @fission-ai/openspec@${expectedCli}`,
);
// Store presence is checked by the CLI itself: an unknown store must fail, not fall back silently.
try {
run("openspec", ["validate", "--specs", "--store", store]);
} catch (error) {
assert.fail(`openspec store '${store}' is not usable: ${error.stderr || error.stdout || error.message}`);
}
console.log(`openspec ${cliVersion} on Node ${expectedNode}: toolchain matches`);