spec: unify income payment cards and drop period comparisons (WealthLedger/main#33) (#37)
Specs security / security (push) Successful in 9s
Specs verify / verify (push) Successful in 12s

spec: unify income payment cards and drop period comparisons (WealthLedger/main#33)
This commit was merged in pull request #37.
This commit is contained in:
2026-10-13 18:16:27 +03:00
parent fe295aa73a
commit 6c858a8858
14 changed files with 1014 additions and 0 deletions
@@ -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