docs(specs): archive income analytics change
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: wealth-ledger-sdd
|
||||
created: 2026-08-27
|
||||
@@ -0,0 +1,126 @@
|
||||
## Context
|
||||
|
||||
Account Ledger уже предоставляет защищённые cursor pages исполненных T-Bank operations, нормализует Money в точные decimal strings, обогащает доступную identity инструмента и хранит до 100 страниц в существующем 30-секундном in-process LRU. Future Cash Events уже формируются backend как полный либо ошибочный 30-дневный прогноз, а Portfolio overview загружает Portfolio, последние операции и будущие события независимыми React Query queries с локальными section states.
|
||||
|
||||
Новая capability должна агрегировать до трёх исторических диапазонов, но не может переносить финансовую арифметику, account authorization, cursor traversal или T-Bank credentials в браузер. Она также не должна сохранять operation history, смешивать source currencies или превращать ошибку forecast в ошибку realized analytics. HTTP contract остаётся Nest-generated OpenAPI; frontend использует только generated types и typed query client. Backend работает под действующим global T-Bank rate limiter и существующей upstream error translation.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Сосредоточить validation дат, complete cursor traversal, exact-type selection, decimal aggregation, comparisons и calendar buckets на backend.
|
||||
- Дать overview дешёвую current-month projection, не загружающую year-over-year, trend и drill-down data.
|
||||
- Обеспечить traceability: breakdown и exact Ledger Entries должны происходить из одного успешно собранного primary result.
|
||||
- Сохранить forecast отдельным query и presentation state, переиспользовав существующий Future Cash Events flow.
|
||||
- Встроить страницу в текущие frontend FSD, route, breadcrumb, ViewModel и section-state patterns без новой chart dependency.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Новый storage, background synchronization или локальная история операций.
|
||||
- Новый T-Bank/MOEX client, distributed cache, job queue либо отдельный analytics service process.
|
||||
- Изменение публичного поведения полного Account Ledger, Portfolio Snapshot или страницы «Будущие события».
|
||||
- FX conversion, historical Portfolio valuation, tax model или performance metrics.
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Backend предоставляет две realized projections с общей aggregation core
|
||||
|
||||
Появляются два защищённых read-only API:
|
||||
|
||||
- `GET /api/v1/brokerage-accounts/:accountId/income-summary` возвращает только current-month source-currency totals для overview;
|
||||
- `GET /api/v1/brokerage-accounts/:accountId/income-analytics?from=YYYY-MM-DD&to=YYYY-MM-DD` возвращает applied period, selected summary, previous/year comparisons, bucket granularity и buckets, instrument breakdown и qualifying primary-period Ledger Entries.
|
||||
|
||||
Оба controller methods делегируют одному income analytics service. Summary вызывает только сбор current-month range; полная projection собирает primary, previous и, когда календарно и по opened date доступен, year-ago range. Это не заставляет каждое открытие Portfolio overview читать до трёх диапазонов и не создаёт query mode/configuration для фиксированного summary behavior.
|
||||
|
||||
Альтернатива — один всегда полный endpoint — отклонена из-за ненужной upstream нагрузки на overview. Расчёт на frontend отклонён, потому что потребовал бы cursor orchestration и дублировал бы exact decimal/calendar/security invariants за публичной trust boundary.
|
||||
|
||||
### 2. Complete-range collection расширяет существующий Account Ledger seam
|
||||
|
||||
`BrokerageOperationsService` получает внутреннюю complete-range operation, которая один раз подтверждает T-Bank connection и доступность Brokerage Account, затем последовательно проходит cursor chain через существующую page loading/mapping/cache logic. Каждый page request остаётся `OPERATION_STATE_EXECUTED`; server-side category `income` уменьшает локальную обработку, а income service после mapping принимает только exact upstream types `OPERATION_TYPE_COUPON` и `OPERATION_TYPE_DIVIDEND`.
|
||||
|
||||
Collector завершает range только при terminal page. `hasNext` без непустого нового cursor и повтор cursor считаются upstream failure, чтобы malformed chain не дал partial result или бесконечный цикл. Три независимых range chains полной аналитики могут выполняться параллельно, но каждая chain остаётся последовательной, а существующий global T-Bank limiter ограничивает фактические RPC starts. Любая необходимая range failure отклоняет realized response целиком.
|
||||
|
||||
Существующий page LRU остаётся единственным operation cache: access check выполняется до cache reuse, summary и full analytics могут переиспользовать равные свежие pages, а новый result cache не появляется. Прямые дополнительные вызовы `TbankClient` из income service отклонены как дублирование authorization, mapping, metadata fallback, error translation и cache identity.
|
||||
|
||||
### 3. Date-only calendar model отделён от instants upstream
|
||||
|
||||
Public query принимает только строгие ISO calendar dates `YYYY-MM-DD`. Backend валидирует реальную дату, `from <= to`, inclusive duration не более 366 days и отсутствие будущих дат относительно Europe/Moscow. Today получается через `Intl.DateTimeFormat` с `Europe/Moscow`; date arithmetic выполняется над date-only values через UTC-safe calendar helpers, а только на T-Bank boundary диапазон преобразуется в точные Moscow start/end instants.
|
||||
|
||||
Полный календарный месяц распознаётся по первому и последнему дню; previous range для него является предыдущим полным месяцем. В остальных случаях previous range имеет ту же inclusive duration. Year-ago range использует те же month/day; невозможная дата, например 29 февраля в невисокосном году, либо range до `Brokerage Account.openedDate` даёт `yearComparison: null`. Успешно полученный полный range без операций означает zero, тогда как отсутствующая comparison не подменяется нулём.
|
||||
|
||||
Timeline buckets рассчитываются backend по Europe/Moscow: day для 1–45 days, ISO calendar week Monday–Sunday для 46–180 days и calendar month для более длинного периода. Первый и последний week/month bucket обрезаются applied range, а все промежуточные empty buckets создаются явно.
|
||||
|
||||
Альтернатива с browser-local timezone отклонена: она дала бы разные границы API, comparison и chart при иной системной timezone User.
|
||||
|
||||
### 4. Exact Money и grouping остаются server-side
|
||||
|
||||
Существующий decimal module расширяется parse operation для decimal strings, чтобы income aggregation и Future Cash Events totals использовали одну BigInt-based реализацию без `number`. Response хранит Money как ISO currency плюс canonical decimal string. Comparison использует union валют двух успешно рассчитанных ranges; отсутствие выплат одной стороны становится exact zero только после полного успешного чтения этой стороны.
|
||||
|
||||
Instrument group key выбирается в порядке instrument UID, FIGI, ticker с instrument type, затем operation-specific fallback. Последний вариант не объединяет неизвестные инструменты только из-за одинакового пустого identifier. Display identity предпочитает доступные mapped instrument данные и сохраняет понятный operation fallback. Сортировка combined totals выполняется exact decimal comparator, не floating-point conversion.
|
||||
|
||||
Primary response включает только qualifying coupon/dividend entries выбранного периода и связывает breakdown rows с их group key. Frontend раскрывает эти уже полученные entries и не делает второй upstream request. Это гарантирует, что drill-down воспроизводит тот же immutable response, включая отрицательные corrections. Альтернатива с отдельным drill-down endpoint отклонена из-за дополнительной нагрузки и риска получить другой срез после истечения cache.
|
||||
|
||||
### 5. Forecast использует существующий Events contract как отдельный query
|
||||
|
||||
`UpcomingEventsDto` получает additive `incomeTotalsByCurrency`, рассчитанный backend только из coupon/dividend events с известным `projectedAmount`. Existing events list, sorting, completeness и error semantics не меняются; текущая полная Events page продолжает показывать список без total. Analytics ViewModel использует тот же `/events` query, выбирает три ближайших coupon/dividend events и получает exact totals из нового поля.
|
||||
|
||||
Так realized endpoint не зависит от Portfolio/MOEX forecast path, а forecast query может самостоятельно перейти в loading, empty или error state. Новый отдельный forecast endpoint отклонён как дублирующая projection поверх уже подходящего полного Events response; frontend aggregation отклонена, чтобы Money arithmetic не покидала backend.
|
||||
|
||||
### 6. Frontend следует FSD и URL-as-applied-state
|
||||
|
||||
Новый income entity slice выводит generated response types и два React Query hooks для summary и full analytics. Account overview ViewModel подключает summary query независимо от Portfolio, operations и events и передаёт отдельной section view discriminated local state. Section присутствует и в empty, и в loaded Portfolio outcomes; её failure не меняет top-level Portfolio state.
|
||||
|
||||
Account-scoped route `/accounts/:accountId/income` получает page container, один ViewModel hook и отдельные loading/error/empty/loaded section views. Breadcrumb строится как Accounts → selected account → «Пассивный доход»; глобальная navigation не меняется.
|
||||
|
||||
Applied `from`/`to` принадлежат URL search params. ViewModel нормализует отсутствующий period к Moscow current month, а initial/default/reset state записывает canonical explicit dates через history replace. Draft period живёт в `react-hook-form` с Zod validation и не меняет query key до submit. Apply обновляет URL; browser Back/Forward восстанавливает applied query и синхронизирует draft. React Query использует 30-second stale time, отключённые automatic retry/focus refetch и Client Cached Snapshot feedback в соответствии с существующей policy.
|
||||
|
||||
### 7. Visualization строится без новой chart dependency
|
||||
|
||||
Backend-provided buckets отображаются лёгкой semantic section view: CSS/SVG stacked columns имеют общую zero baseline, отдельные positive/negative coupon и dividend segments и меняются явным currency control. Масштабирование в presentation использует только derived ratios; authoritative decimal strings форматируются отдельно и не пересчитываются через floating point.
|
||||
|
||||
Графика скрыта от accessibility tree как дублирующее представление. Рядом остаётся структурированный текстовый список buckets с датой/периодом и exact coupon/dividend values; currency control имеет доступное имя и keyboard behavior текущих Mantine controls. Empty buckets сохраняются в обеих формах. Instrument rows раскрывают existing `LedgerEntryRow` instances для exact entries.
|
||||
|
||||
Установленная frontend dependency не предоставляет chart component. Добавление chart library отклонено: один stacked-column use case с заранее рассчитанными buckets не оправдывает новый runtime dependency и bundle/maintenance surface.
|
||||
|
||||
## Delivery Map
|
||||
|
||||
| Area | Status | Architectural outcome or N/A reason |
|
||||
| --- | --- | --- |
|
||||
| frontend | `affected` | Generated API types и income entity queries; account income route/ViewModel/state views; URL-bound period form; accessible per-currency trend, breakdown/drill-down и forecast sections; independent overview summary. |
|
||||
| backend | `affected` | Complete-range Ledger seam, date/decimal aggregation core, summary и full analytics endpoints, additive Future Cash Events income totals и Nest OpenAPI DTOs. |
|
||||
| specs | `affected` | Canonical Proposal, Delta Spec, Design и последующий Tasks/Apply lifecycle для `brokerage-account-income-analytics`. |
|
||||
| docs | `affected` | После поставки as-built feature flow, API boundaries, domain terms и system overview отражают realized analytics, overview summary и separate forecast без копирования planning artifacts. |
|
||||
|
||||
### Contract Gate Decision
|
||||
|
||||
| Gate | Status | Reason |
|
||||
| --- | --- | --- |
|
||||
| runtime-openapi | `required` | Backend добавляет два endpoints и поле Events response, а frontend добавляет их generated contract consumers; runtime OpenAPI, committed snapshot и generated types должны совпасть. |
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- Один full analytics request может прочитать много upstream pages для primary, previous и year-ago ranges. Максимум 366 days, exact-type filtering, page size и global limiter ограничивают работу, но latency остаётся выше обычной Ledger page; UI поэтому имеет structural loading и cached-snapshot feedback.
|
||||
- LRU хранит pages, а не атомарный multi-page snapshot. Complete cursor traversal предотвращает partial result, но upstream correction между pages теоретически может изменить историю; response фиксирует собственный calculation time и не обещает более сильную snapshot isolation, которой source contract не предоставляет.
|
||||
- T-Bank не публикует гарантированную глубину history. Backend считает успешно пройденный range доступным; known pre-opened-date year range становится unavailable. Он не пытается угадать скрытую retention boundary.
|
||||
- Qualifying primary entries увеличивают response size, зато обеспечивают exact drill-down без второго запроса. Scope ограничен только coupon/dividend entries максимум за 366 days; при подтверждённом payload pressure отдельный paginated drill-down потребует нового Design.
|
||||
- Additive Events total расширяет public contract, хотя существующая Events page его не показывает. Это уменьшает endpoints и повторную source orchestration ценой одного неиспользуемого поля для старого consumer.
|
||||
- Signed corrections требуют chart baseline с negative segments. Текстовый bucket list остаётся canonical accessible representation, поэтому визуальная компактность не скрывает точные значения.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Расширить reusable decimal и complete Ledger collection seams, сохранив существующее Ledger API behavior.
|
||||
2. Добавить backend summary/full analytics contracts и additive Events income totals, затем опубликовать обновлённый runtime OpenAPI.
|
||||
3. Синхронизировать committed frontend OpenAPI snapshot и generated types до подключения новых consumers.
|
||||
4. Добавить entity queries, account overview summary, income route/ViewModel и presentation sections; существующие Portfolio, Ledger и Events routes остаются рабочими на каждом шаге.
|
||||
5. После фактической поставки обновить as-built docs и canonical main spec через обычный verify/archive flow.
|
||||
|
||||
### Rollback Contract
|
||||
|
||||
| Field | Decision |
|
||||
| --- | --- |
|
||||
| Status | `required` |
|
||||
| Reason | Change расширяет публичный backend/frontend OpenAPI contract и выполняет денежную агрегацию через новую пользовательскую поверхность. |
|
||||
| Trigger | Contract mismatch, incorrect Money/date aggregation, unacceptable upstream load либо regression существующих Portfolio/Ledger/Events journeys. |
|
||||
| Actions | Сначала убрать frontend income route, overview consumer и зависимость от additive Events field; затем откатить synchronized frontend contract artifacts и backend analytics endpoints/Events field. Complete-range/decimal refactor откатывается либо сохраняется только при доказанном сохранении существующего Ledger behavior. |
|
||||
| Data safety | Analytics read-only и ничего не сохраняет; rollback не мигрирует и не удаляет User, connection, Portfolio, operations или event data. Existing short-lived caches можно безопасно потерять при restart. |
|
||||
| Verification | Подтвердить совпадение runtime OpenAPI с frontend snapshot, отсутствие новых route/field consumers и сохранение прежних Account overview, full Ledger и Future Cash Events behaviors через affected canonical gates. |
|
||||
@@ -0,0 +1,32 @@
|
||||
## Why
|
||||
|
||||
User вынужден вручную находить купонные и дивидендные выплаты в журнале операций и суммировать их, чтобы оценить фактически полученный пассивный доход за месяц или другой период. Это затрудняет регулярную проверку поступлений и не позволяет быстро перейти от итога к операциям, из которых он сложился. WealthLedger уже показывает исполненные операции и известные будущие события выбранного Brokerage Account, поэтому следующим продуктовым шагом должна стать их понятная, проверяемая аналитика без смешения факта и прогноза.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Для одного выбранного Brokerage Account появляется страница «Пассивный доход» с аналитикой фактически полученных купонов и дивидендов за применённый период.
|
||||
- Итоги, разбивка по видам дохода, динамика и состав инструментов показываются отдельно по исходным валютам без конвертации и позволяют перейти к точным операциям, включённым в результат.
|
||||
- User может выбрать включительный период не более 366 дней; по умолчанию используется текущий месяц по сегодняшний день. Изменения периода применяются явно, отражаются в URL и могут быть сброшены к значению по умолчанию.
|
||||
- Результат сопоставляется с непосредственно предшествующим периодом и, когда данные доступны, с теми же календарными датами годом ранее. Сопоставление показывает денежные значения и разницы, но не оценивает инвестиционную эффективность.
|
||||
- Купонные и дивидендные корректировки сохраняют знак исходной выплаты. Налоги и другие операции не включаются в итог; интерфейс объясняет эту границу без утверждений о суммах «до налогов», «после налогов», gross или net.
|
||||
- На обзоре Brokerage Account появляется компактный итог купонов и дивидендов с начала текущего месяца по сегодняшний день и переход к полной аналитике. Этот итог не наследует пользовательский период со страницы аналитики.
|
||||
- Отдельный прогнозный блок показывает известные купоны и дивиденды на следующие 30 дней, не смешивая их с фактическим доходом, подчёркивает неопределённость прогноза и ведёт к полному списку будущих событий.
|
||||
- Если для успешного расчёта недоступна часть необходимых операций, система показывает восстанавливаемую ошибку вместо частичного итога. Ошибка прогноза остаётся локальной и не скрывает успешно рассчитанный фактический доход.
|
||||
- Вне scope остаются сводная аналитика по нескольким Brokerage Account, ручные доходные записи и корректировки, расчёт или атрибуция налогов, валютная конвертация и общий мультивалютный итог, локальное хранение истории операций, историческая оценка Portfolio и общая аналитика инвестиционной эффективности.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `brokerage-account-income-analytics`: Проверяемая аналитика фактически полученных купонов и дивидендов одного Brokerage Account, её current-month summary и отдельный 30-дневный прогноз известных поступлений.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
<!-- Existing capability Requirements не изменяются. -->
|
||||
|
||||
## Impact
|
||||
|
||||
- User получает автоматический ответ на вопрос, сколько купонов и дивидендов фактически поступило на выбранный Brokerage Account за интересующий период, и может проверить состав результата по операциям.
|
||||
- Обзор выбранного Brokerage Account дополняется текущим месячным итогом, а account-scoped navigation — переходом к полной аналитике.
|
||||
- Расчёт затрагивает account ledger и использует существующие данные о будущих событиях, сохраняя их текущую доменную семантику и границы доступности.
|
||||
- Изменение требует согласованной поставки пользовательского интерфейса, серверного поведения, canonical specification и as-built документации.
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
## Purpose
|
||||
|
||||
Позволяет User анализировать фактически полученные купоны и дивиденды одного Brokerage Account по исходным валютам, проверять состав результата и отдельно видеть известный ближайший прогноз.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Realized income analytics is account-scoped and period-bound
|
||||
|
||||
Система MUST предоставлять аутентифицированному User аналитику фактически полученного дохода только для одного открытого поддерживаемого Brokerage Account, доступного через текущий T-Bank connection User. Применённый период MUST задаваться включительными календарными датами `from` и `to` в Europe/Moscow, MUST NOT начинаться после своего окончания, MUST NOT содержать будущую дату и MUST NOT превышать 366 дней. При отсутствии применённого периода система MUST использовать интервал от первого дня текущего месяца до сегодняшнего дня включительно.
|
||||
|
||||
#### Scenario: Аналитика за период по умолчанию
|
||||
|
||||
- **WHEN** аутентифицированный User запрашивает аналитику доступного Brokerage Account без периода
|
||||
- **THEN** система рассчитывает результат от первого дня текущего месяца по Europe/Moscow до сегодняшнего дня включительно
|
||||
|
||||
#### Scenario: Аналитика за допустимый пользовательский период
|
||||
|
||||
- **WHEN** User применяет включительный период не более 366 дней без будущих дат
|
||||
- **THEN** система рассчитывает аналитику доступного Brokerage Account за все календарные даты этого периода по Europe/Moscow
|
||||
|
||||
#### Scenario: Отклонение недопустимого периода
|
||||
|
||||
- **WHEN** User запрашивает период длиннее 366 дней, с будущей датой либо с `from` после `to`
|
||||
- **THEN** система отклоняет запрос и не возвращает результат для другого или усечённого периода
|
||||
|
||||
#### Scenario: Отклонение недоступного счёта
|
||||
|
||||
- **WHEN** User запрашивает аналитику Brokerage Account, который не является открытым поддерживаемым счётом, доступным через его текущий T-Bank connection
|
||||
- **THEN** система возвращает существующий ответ о недоступности счёта и не раскрывает его доход или операции
|
||||
|
||||
### Requirement: Realized income uses exact executed coupon and dividend payments
|
||||
|
||||
Система MUST рассчитывать фактически полученный доход только по исполненным Ledger Entries с точным upstream-типом coupon или dividend. Для каждой исходной валюты система MUST отдельно возвращать общий итог, купонный subtotal и дивидендный subtotal как точные суммы исходных broker-reported payments. Знак каждой выплаты MUST сохраняться, поэтому отрицательная корректировка MUST уменьшать соответствующий итог и оставаться доступной в составе результата. Dividend transfer, погашение principal, offer, trade, transfer, fee, tax, overnight income, иной тип операции, нереализованный результат и изменение рыночной цены MUST NOT входить в фактически полученный доход. Система MUST NOT выполнять валютную конвертацию или формировать общий итог по разным валютам.
|
||||
|
||||
#### Scenario: Купоны и дивиденды образуют раздельные subtotals
|
||||
|
||||
- **WHEN** выбранный период содержит исполненные купонные и дивидендные выплаты в одной валюте
|
||||
- **THEN** система возвращает их точные раздельные subtotals и общий итог, равный их сумме в этой валюте
|
||||
|
||||
#### Scenario: Отрицательная корректировка сохраняет знак
|
||||
|
||||
- **WHEN** исполненная coupon или dividend Ledger Entry содержит отрицательную broker-reported payment
|
||||
- **THEN** система включает её с исходным знаком, уменьшает соответствующие subtotal и total и сохраняет запись доступной для проверки
|
||||
|
||||
#### Scenario: Налоги и иные операции не входят в доход
|
||||
|
||||
- **WHEN** выбранный период наряду с coupon и dividend содержит tax, fee, trade, transfer, maturity или иной тип Ledger Entry
|
||||
- **THEN** система не включает эти записи в coupon subtotal, dividend subtotal или total
|
||||
|
||||
#### Scenario: Несколько валют остаются разделёнными
|
||||
|
||||
- **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 заменить применённый период, загрузить соответствующий результат и представить применённые даты в URL страницы. Сброс MUST немедленно вернуть применённый период к текущему месяцу по сегодняшний день и синхронизировать URL. Перезагрузка или открытие URL с допустимым периодом MUST восстановить тот же применённый период. Действие перехода на страницу MUST быть доступно из выбранного Brokerage Account, а не из глобальной навигации.
|
||||
|
||||
#### Scenario: Черновик периода не применён
|
||||
|
||||
- **WHEN** User изменяет черновик `from` или `to`, но не применяет его
|
||||
- **THEN** видимые итоги, сравнения, динамика и breakdown остаются основанными на последнем применённом периоде
|
||||
|
||||
#### Scenario: Применение периода
|
||||
|
||||
- **WHEN** User явно применяет допустимый черновик периода
|
||||
- **THEN** страница загружает аналитику этого периода и отражает применённые `from` и `to` в URL
|
||||
|
||||
#### Scenario: Восстановление применённого периода из URL
|
||||
|
||||
- **WHEN** User открывает URL страницы с допустимыми `from` и `to`
|
||||
- **THEN** страница показывает аналитику за указанный применённый период
|
||||
|
||||
#### Scenario: Сброс периода
|
||||
|
||||
- **WHEN** User сбрасывает пользовательский период
|
||||
- **THEN** страница немедленно применяет период от первого дня текущего месяца по Europe/Moscow до сегодняшнего дня и синхронизирует URL
|
||||
|
||||
### Requirement: Analytics summary and trend remain readable per currency
|
||||
|
||||
Страница MUST обозначать фактический результат как «Получено купонами и дивидендами» и для каждой исходной валюты показывать total, coupon subtotal и dividend subtotal выбранного периода. Она MUST объяснять, что tax operations не включены, не называя фактический результат gross, net, суммой до налогов, суммой после налогов или налоговым расчётом. Страница MUST показывать stacked column visualization с отдельными coupon и dividend значениями: период до 45 дней включительно группируется по календарным дням, период от 46 до 180 дней включительно — по календарным неделям, более длинный период — по календарным месяцам. Пустые buckets внутри периода MUST оставаться видимыми с нулевыми значениями. При нескольких валютах явный currency control MUST переключать visualization между ними, а эквивалентные значения MUST оставаться доступными как читаемый текст, не только как графика, цвет или положение.
|
||||
|
||||
#### Scenario: Короткий период группируется по дням
|
||||
|
||||
- **WHEN** включительная продолжительность выбранного периода не превышает 45 дней
|
||||
- **THEN** visualization показывает отдельные календарные day buckets с coupon и dividend значениями и сохраняет пустые дни как нулевые buckets
|
||||
|
||||
#### Scenario: Средний период группируется по неделям
|
||||
|
||||
- **WHEN** включительная продолжительность выбранного периода составляет от 46 до 180 дней
|
||||
- **THEN** visualization показывает calendar week buckets с раздельными coupon и dividend значениями
|
||||
|
||||
#### Scenario: Длинный период группируется по месяцам
|
||||
|
||||
- **WHEN** включительная продолжительность выбранного периода превышает 180 дней
|
||||
- **THEN** visualization показывает calendar month buckets с раздельными coupon и dividend значениями
|
||||
|
||||
#### Scenario: User переключает валюту visualization
|
||||
|
||||
- **WHEN** результат содержит подходящие выплаты в нескольких валютах и User выбирает одну из них
|
||||
- **THEN** visualization и его текстовый эквивалент показывают значения только выбранной исходной валюты без изменения итогов остальных валют
|
||||
|
||||
### Requirement: Instrument breakdown is traceable to exact Ledger Entries
|
||||
|
||||
Для каждой исходной валюты страница MUST показывать instrument breakdown, отсортированный по combined total по убыванию. Каждая строка MUST содержать доступную identity Instrument, coupon subtotal, dividend subtotal, combined total и количество включённых выплат. User MUST иметь возможность перейти от строки к точным исполненным Ledger Entries выбранных Instrument, валюты и периода, которые образовали её значения; drill-down MUST сохранять отрицательные корректировки и MUST NOT включать записи вне аналитического итога.
|
||||
|
||||
#### Scenario: Breakdown показывает состав дохода по инструментам
|
||||
|
||||
- **WHEN** выбранный период содержит подходящие выплаты нескольких Instrument в одной валюте
|
||||
- **THEN** страница показывает строки по убыванию combined total с identity, двумя subtotals, combined total и receipt count каждого Instrument
|
||||
|
||||
#### Scenario: Drill-down воспроизводит строку breakdown
|
||||
|
||||
- **WHEN** User открывает drill-down строки Instrument
|
||||
- **THEN** он видит точные coupon и dividend Ledger Entries этого Instrument, периода и валюты, сумма и количество которых соответствуют строке
|
||||
|
||||
### 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 возвращать нулевой результат и отображаться с явным объяснением пустого состояния.
|
||||
|
||||
#### Scenario: Все страницы операций успешно получены
|
||||
|
||||
- **WHEN** для расчёта требуется несколько cursor pages и каждая страница успешно получена
|
||||
- **THEN** система формирует результат по всем подходящим Ledger Entries со всех страниц
|
||||
|
||||
#### Scenario: Одна из страниц операций недоступна
|
||||
|
||||
- **WHEN** получение любой необходимой cursor page завершается ошибкой
|
||||
- **THEN** страница показывает восстанавливаемую ошибку и не показывает частичный результат как полный
|
||||
|
||||
#### Scenario: В периоде нет купонов и дивидендов
|
||||
|
||||
- **WHEN** все необходимые операции успешно получены, но среди них нет подходящих coupon или dividend Ledger Entries
|
||||
- **THEN** страница показывает нулевой результат и объясняет, что за выбранный период купоны и дивиденды не найдены
|
||||
|
||||
### Requirement: Account overview has an independent current-month income summary
|
||||
|
||||
Обзор выбранного Brokerage Account MUST показывать компактный source-currency summary «Получено купонами и дивидендами» за период от первого дня текущего месяца по Europe/Moscow до сегодняшнего дня включительно и действие перехода на страницу «Пассивный доход». Summary MUST всегда использовать этот current-month период и MUST NOT наследовать последний пользовательский период страницы аналитики. Пустой или неуспешный расчёт summary MUST оставаться локальным состоянием блока и MUST NOT заменять успешно отображённый Portfolio или другие блоки обзора.
|
||||
|
||||
#### Scenario: Обзор показывает доход текущего месяца
|
||||
|
||||
- **WHEN** обзор Brokerage Account загружен и current-month analytics успешно рассчитана
|
||||
- **THEN** он показывает отдельные source-currency итоги с начала текущего месяца по сегодняшний день и переход к полной аналитике
|
||||
|
||||
#### Scenario: Пользовательский период не влияет на обзор
|
||||
|
||||
- **WHEN** User ранее применил на странице аналитики период, отличный от текущего месяца
|
||||
- **THEN** summary обзора по-прежнему рассчитывается от первого дня текущего месяца по сегодняшний день
|
||||
|
||||
#### Scenario: Current-month summary недоступен
|
||||
|
||||
- **WHEN** Portfolio обзора успешно загружен, но расчёт current-month summary завершается ошибкой
|
||||
- **THEN** Portfolio и остальные успешно загруженные блоки остаются доступными, а summary показывает собственное состояние восстановления
|
||||
|
||||
### Requirement: Forecast is separate from realized income
|
||||
|
||||
Страница «Пассивный доход» MUST показывать семантически и визуально отдельный forecast block известных coupon и dividend Future Cash Events на следующие 30 дней от сегодняшней даты независимо от выбранного исторического периода. Forecast totals MUST включать только события coupon и dividend с известной projected amount и MUST оставаться разделёнными по исходным валютам; maturity и offer MUST NOT входить в эти totals. Блок MUST показывать три ближайших подходящих события, обозначать результат как не гарантированный прогноз и предоставлять переход к существующей полной странице «Будущие события». Forecast values MUST NOT входить в realized total, comparisons, trend или instrument breakdown. Ошибка forecast MUST оставаться локальной для блока и MUST NOT скрывать успешно загруженную realized analytics.
|
||||
|
||||
#### Scenario: Forecast показывает ближайшие известные выплаты
|
||||
|
||||
- **WHEN** на следующие 30 дней известны coupon или dividend Future Cash Events
|
||||
- **THEN** блок показывает не более трёх ближайших подходящих событий, source-currency totals известных projected amounts, предупреждение о неопределённости и переход к полному списку будущих событий
|
||||
|
||||
#### Scenario: События без суммы не искажают forecast total
|
||||
|
||||
- **WHEN** подходящее coupon или dividend Future Cash Event не имеет известной projected amount
|
||||
- **THEN** событие не добавляет нулевую или вымышленную сумму в forecast total
|
||||
|
||||
#### Scenario: Maturity и offer не входят в forecast income
|
||||
|
||||
- **WHEN** ближайшие Future Cash Events содержат maturity или offer
|
||||
- **THEN** forecast block не включает их в coupon или dividend totals
|
||||
|
||||
#### Scenario: Пользовательский исторический период не влияет на forecast
|
||||
|
||||
- **WHEN** User применяет любой допустимый исторический период
|
||||
- **THEN** forecast block по-прежнему охватывает следующие 30 дней от сегодняшней даты
|
||||
|
||||
#### Scenario: Forecast недоступен при успешной realized analytics
|
||||
|
||||
- **WHEN** realized analytics успешно загружена, а запрос forecast завершается ошибкой
|
||||
- **THEN** realized totals, comparisons, trend и breakdown остаются доступными, а forecast block показывает собственную ошибку
|
||||
@@ -0,0 +1,83 @@
|
||||
## 1. Базовые backend-механизмы операций и расчётов
|
||||
|
||||
- [x] 1.1 [backend] Расширить существующий сервис Account Ledger проверяющим доступ сборщиком полного диапазона, который переиспользует mapping страниц и LRU, проходит каждую cursor chain до terminal page и отклоняет отсутствующий либо повторяющийся continuation cursor; сохранить поведение публичного Ledger endpoint и добавить целевые regression tests.
|
||||
- **Covers:** `brokerage-account-income-analytics` — `Realized income analytics is account-scoped and period-bound`; `brokerage-account-income-analytics` — `Realized analytics never presents an incomplete result as complete`
|
||||
- **Verification:** Запустить целевые backend-наборы Jest для проверки ownership/cache в BrokerageOperationsService, успешного сбора нескольких страниц, распространения ошибки страницы, отсутствующего cursor и повторяющегося cursor.
|
||||
- [x] 1.2 [backend] Расширить общие BigInt decimal utilities каноническим parsing/comparison десятичных строк и реализовать date-only планирование диапазонов в Europe/Moscow для периода по умолчанию и пользовательского периода, сравнения с предыдущим периодом/полным месяцем, доступности year-ago и day/week/month buckets; добавить risk-based unit tests.
|
||||
- **Covers:** `brokerage-account-income-analytics` — `Realized income analytics is account-scoped and period-bound`; `brokerage-account-income-analytics` — `Period comparisons preserve calendar and currency meaning`; `brokerage-account-income-analytics` — `Analytics summary and trend remain readable per currency`
|
||||
- **Verification:** Запустить целевые backend-тесты Jest для некорректных, обратных, будущих и превышающих 366 дней диапазонов, границ месяца по Москве, equal-duration и full-month comparisons, leap-day/pre-opened year comparison, порогов buckets и точной signed decimal arithmetic.
|
||||
|
||||
## 2. Backend-проекции дохода и публичный контракт
|
||||
|
||||
- [x] 2.1 [backend] Реализовать общую income aggregation core и защищённые проекции current-month summary/full analytics: выбирать только точные типы coupon/dividend, сохранять исходные валюты и знаки, формировать union для comparisons, empty buckets, точные группы инструментов и включённые primary entries.
|
||||
- **Covers:** `brokerage-account-income-analytics` — `Realized income analytics is account-scoped and period-bound`; `brokerage-account-income-analytics` — `Realized income uses exact executed coupon and dividend payments`; `brokerage-account-income-analytics` — `Period comparisons preserve calendar and currency meaning`; `brokerage-account-income-analytics` — `Analytics summary and trend remain readable per currency`; `brokerage-account-income-analytics` — `Instrument breakdown is traceable to exact Ledger Entries`; `brokerage-account-income-analytics` — `Realized analytics never presents an incomplete result as complete`; `brokerage-account-income-analytics` — `Account overview has an independent current-month income summary`
|
||||
- **Verification:** Запустить целевые backend-тесты сервиса для исключения неточных типов, отрицательных корректировок, мультивалютных итогов без grand total, нулевых результатов, differences сравнений, полноты buckets, стабильного instrument fallback/group ordering, сверки drill-down и поведения источника «полностью или ошибка».
|
||||
- [x] 2.2 [backend] Добавить Nest DTO/controller contracts для `GET /api/v1/brokerage-accounts/:accountId/income-summary` и `GET /api/v1/brokerage-accounts/:accountId/income-analytics`, включая строгую валидацию date-only query, существующую семантику auth/account errors и Swagger schemas.
|
||||
- **Covers:** `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`
|
||||
- **Verification:** Запустить controller/DTO tests, проверяющие передачу аутентифицированного User, принятие периода по умолчанию и валидных дат, отклонение некорректных inputs, обработку недоступного счёта и сгенерированные response shapes.
|
||||
- [x] 2.3 [backend] Добавить точный расчёт `incomeTotalsByCurrency` в существующий полный ответ Future Cash Events, учитывая только события coupon/dividend с известным projected Money и не меняя ordering событий, поведение списка и семантику ошибок.
|
||||
- **Covers:** `brokerage-account-income-analytics` — `Forecast is separate from realized income`
|
||||
- **Verification:** Запустить regression tests UpcomingEventsService для итогов coupon/dividend, неизвестных сумм, исключения maturity/offer, нескольких исходных валют, ошибки источника и неизменившихся ordering/content существующих событий.
|
||||
- [x] 2.4 [backend] Добавить или расширить backend risk-based tests на границах service/HTTP contract для authorization, точного Money, полного cursor traversal и обратной совместимости additive public response.
|
||||
- **Covers:** `brokerage-account-income-analytics` — `Realized income analytics is account-scoped and period-bound`; `brokerage-account-income-analytics` — `Realized income uses exact executed coupon and dividend payments`; `brokerage-account-income-analytics` — `Realized analytics never presents an incomplete result as complete`; `brokerage-account-income-analytics` — `Forecast is separate from realized income`
|
||||
- **Verification:** Запустить затронутые backend-наборы Jest с `--runInBand` и проверить, что существующие regression cases Portfolio, Ledger и Events выполняются вместе с новыми критическими cases для money/security/public contract.
|
||||
|
||||
## 3. Синхронизация контракта OpenAPI
|
||||
|
||||
- [x] 3.1 [frontend] Скопировать сгенерированный backend-документ OpenAPI в committed frontend snapshot и регенерировать frontend schema types до добавления consumers; не редактировать transport DTO вручную.
|
||||
- **Covers:** `N/A` — Это требуемый утверждённым Design мост совместимости для двух новых backend endpoints и additive Events field; он не вводит самостоятельного observable behavior.
|
||||
- **Verification:** Запустить `npm run openapi:generate` в `frontend`, затем проверить, что generated `paths` содержат оба income endpoints и `incomeTotalsByCurrency` без несвязанного contract drift.
|
||||
|
||||
## 4. Frontend-данные, overview и состояние применённого периода
|
||||
|
||||
- [x] 4.1 [frontend] Добавить public API income entity и типизированные React Query hooks для summary/full analytics на основе generated `paths`, с 30-секундным stale time, без automatic retry и через существующий authenticated query client.
|
||||
- **Covers:** `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`
|
||||
- **Verification:** Запустить целевые hook tests для account-scoped path/query parameters, отключённых запросов без account, default summary request, cache identity применённого диапазона и настроенной query policy.
|
||||
- [x] 4.2 [frontend] Интегрировать независимое состояние current-month income summary в ViewModel страницы Account и семантические section views для empty и loaded outcomes Portfolio, включая локальные loading/error/empty states и ссылку на полную аналитику.
|
||||
- **Covers:** `brokerage-account-income-analytics` — `Account overview has an independent current-month income summary`
|
||||
- **Verification:** Запустить tests ViewModel/view страницы Account, доказывающие отображение source-currency summary, фиксированный current-month behavior, локальную ошибку summary и сохранение видимости успешно загруженных Portfolio и соседних sections.
|
||||
- [x] 4.3 [frontend] Реализовать ViewModel страницы дохода счёта с каноническими явными URL-параметрами `from`/`to`, draft state на react-hook-form/Zod, синхронизацией apply/reset и Back/Forward, состоянием полного realized query и независимой проекцией трёх ближайших forecast events из существующего Events query.
|
||||
- **Covers:** `brokerage-account-income-analytics` — `Realized income analytics is account-scoped and period-bound`; `brokerage-account-income-analytics` — `User explicitly applies an analytics period represented in the URL`; `brokerage-account-income-analytics` — `Realized analytics never presents an incomplete result as complete`; `brokerage-account-income-analytics` — `Forecast is separate from realized income`
|
||||
- **Verification:** Запустить целевые tests ViewModel для default URL canonicalization, изоляции draft, корректного apply, reset, восстановления navigation, ограничений для некорректного/будущего/слишком длинного периода, состояний loading/error/empty realized analytics и изоляции ошибки forecast.
|
||||
|
||||
## 5. Frontend-представление аналитики и навигация
|
||||
|
||||
- [x] 5.1 [frontend] Создать принадлежащие странице structural loading/error/empty/loaded views и source-currency sections для selected/previous/year summaries, включая требуемую формулировку налоговой границы и недоступное годовое сравнение без языка investment performance.
|
||||
- **Covers:** `brokerage-account-income-analytics` — `Realized income uses exact executed coupon and dividend payments`; `brokerage-account-income-analytics` — `Period comparisons preserve calendar and currency meaning`; `brokerage-account-income-analytics` — `Realized analytics never presents an incomplete result as complete`
|
||||
- **Verification:** Запустить tests рендеринга страницы для signed corrections, значений coupon/dividend/total по валютам, отсутствующего grand total, недоступного year comparison, явного успешного zero state и recoverable realized error.
|
||||
- [x] 5.2 [frontend] Реализовать без новой зависимости управляемый валютой stacked trend с positive/negative baseline и его структурированный текстовый эквивалент с точными значениями, сохраняя все backend buckets, включая нулевые, для day/week/month granularities.
|
||||
- **Covers:** `brokerage-account-income-analytics` — `Analytics summary and trend remain readable per currency`
|
||||
- **Verification:** Запустить component tests, проверяющие переключение валюты, отдельные segments coupon/dividend, представление отрицательной корректировки, нулевые buckets, доступный с клавиатуры control и читаемые значения buckets независимо от графики/цвета.
|
||||
- [x] 5.3 [frontend] Реализовать per-currency строки instrument breakdown в переданной backend сортировке и раскрываемые точные включённые Ledger Entries с переиспользованием существующего представления строки Ledger.
|
||||
- **Covers:** `brokerage-account-income-analytics` — `Instrument breakdown is traceable to exact Ledger Entries`
|
||||
- **Verification:** Запустить component tests, которые сверяют subtotal/total/count каждой строки с раскрытыми entries, сохраняют отрицательные корректировки и исключают несвязанные entries.
|
||||
- [x] 5.4 [frontend] Добавить семантически отдельную forecast section на основе backend-поля `incomeTotalsByCurrency` и трёх ближайших подходящих Events, с локальными loading/error/empty states, предупреждением о неопределённости и ссылкой на полную страницу Events.
|
||||
- **Covers:** `brokerage-account-income-analytics` — `Forecast is separate from realized income`
|
||||
- **Verification:** Запустить component/page tests для source-currency forecast totals, неизвестного Money, исключения maturity/offer, фиксированного 30-дневного будущего периода, ограничения в три события, независимой ошибки forecast и неизменившегося realized content.
|
||||
- [x] 5.5 [frontend] Зарегистрировать `/accounts/:accountId/income`, lazy export страницы и account breadcrumb trail, предоставить действия перехода только из overview выбранного Account и сохранить глобальную навигацию без изменений.
|
||||
- **Covers:** `brokerage-account-income-analytics` — `User explicitly applies an analytics period represented in the URL`; `brokerage-account-income-analytics` — `Account overview has an independent current-month income summary`
|
||||
- **Verification:** Запустить route/breadcrumb tests для прямого защищённого перехода, иерархии Accounts → account → income, ссылок overview и отсутствия нового элемента глобальной навигации.
|
||||
|
||||
## 6. As-built документация
|
||||
|
||||
- [x] 6.1 [docs] Документировать поставленный поток income analytics счёта, границу realized/forecast, endpoints, семантику source currency/date, поведение полного результата/ошибки и текущие ограничения; обновить применимые domain/system navigation без копирования planning artifacts.
|
||||
- **Covers:** `N/A` — Durable as-built документация требуется утверждённой four-area Delivery Map, но не реализует дополнительного observable product requirement.
|
||||
- **Verification:** Сверить затронутые Markdown links и terminology с поставленным runtime/OpenAPI behavior, затем запустить canonical gate репозитория docs.
|
||||
|
||||
## 7. Gates затронутых областей и cross-contract
|
||||
|
||||
- [x] 7.1 [backend] Запустить canonical gate backend под Node 22 после завершения всей backend-реализации и целевых risk tests.
|
||||
- **Covers:** `N/A` — Это required governance completion gate затронутой backend-области; он проверяет реализацию, уже сопоставленную с утверждёнными Requirements.
|
||||
- **Verification:** Запустить `npm run verify` в `backend` под Node 22.
|
||||
- [x] 7.2 [frontend] Запустить canonical gate frontend под Node 22 после генерации OpenAPI, реализации route/UI и целевых tests.
|
||||
- **Covers:** `N/A` — Это required governance completion gate затронутой frontend-области; он проверяет реализацию, уже сопоставленную с утверждёнными Requirements.
|
||||
- **Verification:** Запустить `npm run verify` в `frontend` под Node 22.
|
||||
- [x] 7.3 [docs] Запустить canonical gate docs под Node 22 после согласования as-built документации с поставленным поведением.
|
||||
- **Covers:** `N/A` — Это required governance completion gate затронутой docs-области; он проверяет утверждённый documentation delivery outcome.
|
||||
- **Verification:** Запустить `npm run verify` в `docs` под Node 22.
|
||||
- [x] 7.4 [specs] Сверить отметки tasks только с evidence завершённой реализации и запустить strict canonical change validation без создания последующих artifacts или archive evidence.
|
||||
- **Covers:** `N/A` — Это поддерживает canonical SDD authority и предоставляет gate затронутой specs-области; observable behavior реализуют backend/frontend tasks.
|
||||
- **Verification:** Запустить `openspec validate portfolio-income-analytics --type change --strict --store wealth-ledger-specs` из `specs` и проверить, что у каждого завершённого checkbox есть соответствующее delivered evidence.
|
||||
- [x] 7.5 [workspace] Проверить синхронизированный двусторонний runtime OpenAPI contract после публикации backend и генерации frontend snapshot/types.
|
||||
- **Covers:** `N/A` — Cross-repository orchestration контракта требуется утверждённым Design, но не владеет backend- или frontend-поведением.
|
||||
- **Verification:** Запустить `npm run test:openapi-contract` в `backend` под Node 22 против committed frontend OpenAPI snapshot.
|
||||
Reference in New Issue
Block a user