Files
specs/openspec/changes/archive/2026-08-14-add-brokerage-account-upcoming-events/design.md
T
2026-08-14 15:50:35 +03:00

62 lines
6.8 KiB
Markdown

## Context
См. motivation в `proposal.md`. Текущие Portfolio Position и Account Ledger получаются read-through из T-Bank; календаря или сохранённого расписания событий нет. `references/moex-vibe` подтверждает, что MOEX предоставляет дату и величину дивиденда, ближайший купон, дату и номинал погашения, а также дату оферты. Эти данные не содержат расписания будущей амортизации.
## Goals / Non-Goals
**Goals:**
- Вычислять account-scoped прогноз без хранения синтетических событий.
- Сохранять отличия между датой реестра дивиденда и датой денежного события.
- Опубликовать единый OpenAPI-контракт, из которого frontend сгенерирует типы.
**Non-Goals:**
- Расчёт налогов, конвертация валют и общий итог.
- Учёт будущих сделок или владения на дату реестра.
- История фактических выплат, ручное обновление и фильтры.
- Будущие частичные погашения до появления достоверного расписания амортизации.
## Decisions
### Read-through projection of current positions
`GET /api/v1/brokerage-accounts/:accountId/events` получает актуальный Portfolio, обогащает его данными MOEX и собирает только события в rolling-окне 30 календарных дней. Ответ содержит `calculatedAt`; данные не сохраняются.
Это избегает отдельного хранилища и устаревших расписаний. Альтернатива с persisted calendar потребовала бы синхронизации с позициями и не повышает точность, поскольку расчёт всё равно основан на текущем количестве бумаг.
### Explicit event contract
Контракт события содержит тип (`coupon`, `dividend`, `maturity`, `offer`), календарную дату, семантику даты (`event` или `registry`), инструмент, исходную валюту и nullable прогнозную сумму. Для coupon, dividend и maturity сумма равна текущему количеству позиции, умноженному на выплату или номинал на одну бумагу; offer не получает выдуманную сумму.
События возвращаются без агрегированного итога, поскольку валюты не конвертируются. Альтернатива с одним полем даты скрыла бы отличие даты реестра от даты поступления дивиденда.
### Strict source completeness
Если запрос Portfolio или нужные данные MOEX не получены, endpoint отвечает ошибкой и не публикует неполный календарь. Инструмент с успешным ответом, но без поддерживаемого события, просто не добавляет строку.
Это отделяет техническую неполноту от корректного пустого результата. Альтернатива вернуть частичный список не позволяет пользователю отличить пропуск от отсутствия выплат.
### Account-local interface using established list patterns
Frontend добавляет маршрут `/accounts/:accountId/events`, breadcrumb и действие «Будущие события» на обзоре счёта рядом с Account Ledger. Обзор запрашивает тот же endpoint отдельно от Portfolio и показывает первые пять событий в локальном блоке. Страница использует существующие состояния loading/error/empty и компактные responsive rows; она показывает все события результата без локальных фильтров или pagination.
Это продолжает account-scoped навигацию `/accounts/:accountId/operations`. Альтернатива с глобальным календарём смешивала бы события разных счетов и требовала бы выбора счёта.
### Coverage limitation is part of the interface
UI постоянно поясняет, что суммы являются оценками до налогов по текущей позиции, а покрытие зависит от доступных данных MOEX. При пустом списке пояснение дополнительно называет отсутствие известных событий в ближайшие 30 дней. Будущее частичное погашение не имитируется датой maturity.
Это сохраняет полезный минимальный список без ложной точности. Альтернатива скрыть ограничение противоречит природе прогнозов и данным источника.
## Risks / Trade-offs
- [Позиция изменится до события] → обозначать сумму как прогноз по позиции на `calculatedAt`, не заявляя право на выплату.
- [MOEX публикует дивидендную дату реестра, а не дату зачисления] → передавать и отображать отдельную семантику даты.
- [MOEX даёт только ближайший купон] → ограничить окно 30 днями и явно раскрыть неполноту покрытия.
- [Нет графика амортизации] → исключить будущие частичные погашения до добавления надёжного источника.
## Migration Plan
1. Развернуть backend endpoint и обновлённый OpenAPI.
2. Сгенерировать и закоммитить frontend API-контракт, затем выпустить страницу.
3. При откате удалить действие и маршрут frontend; endpoint можно удалить без миграции данных, поскольку состояние не сохраняется.