- Backend: actual events from T-Bank operations, types filter, split forecast/actual summary - Frontend: draft/applied filters with multi-select types, status column (Факт/Прогноз), green actual amounts - Docs: update spec, plan, tasks
273 lines
13 KiB
Markdown
273 lines
13 KiB
Markdown
# Broker Events And Payouts Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development
|
||
> (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use
|
||
> checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** Добавить в брокерский счёт T-Bank раздел предстоящих и прошедших событий с прогнозом
|
||
будущих выплат, фактическими прошедшими поступлениями и управляемыми фильтрами.
|
||
|
||
**Architecture:** Backend предоставляет единый read-only endpoint событий поверх существующих данных
|
||
T-Bank и MOEX. Слой агрегации объединяет прогнозные события по текущим позициям и фактические
|
||
прошедшие выплаты из операций счёта, затем строит раздельный summary факта и прогноза. Frontend
|
||
держит фильтры в черновике, применяет их только по кнопке `Показать` и визуально разделяет
|
||
`Факт`/`Прогноз`.
|
||
|
||
**Tech Stack:** NestJS 10, Prisma, CacheService, MOEX client, T-Bank module, React 18, React Router
|
||
6, TanStack Query 5, TypeScript, Vitest, Testing Library.
|
||
|
||
---
|
||
|
||
## Связанные SDD-артефакты
|
||
|
||
- Epic: `docs/epics/BrokerPortfolio.md`
|
||
- Spec: `docs/features/broker-events-and-payouts/spec.md`
|
||
- Tasks: `docs/features/broker-events-and-payouts/tasks.md`
|
||
|
||
## Gate перед реализацией
|
||
|
||
До начала кода пользователь должен отдельно подтвердить, что:
|
||
|
||
- текущая spec остаётся источником истины;
|
||
- `plan.md` и `tasks.md` согласованы;
|
||
- первая версия действительно ограничивается T-Bank-сценарием и оценкой по текущим позициям;
|
||
- follow-up по entitlement, налогам и полной купонной сетке остаются вне реализации.
|
||
|
||
## Архитектурные решения
|
||
|
||
### Backend endpoint
|
||
|
||
Новый endpoint:
|
||
|
||
```text
|
||
GET /api/v1/broker/accounts/:accountId/events?from=YYYY-MM-DD&to=YYYY-MM-DD&types=dividend,coupon,maturity,offer
|
||
```
|
||
|
||
Endpoint возвращает:
|
||
|
||
- `items` — плоский список событий по текущим позициям счёта;
|
||
- `summary` — агрегаты по фактическим и прогнозным денежным событиям за период;
|
||
- `asOf` — момент построения read model.
|
||
|
||
### Backend read model
|
||
|
||
Новый сервис `BrokerEventsService` живёт внутри `TBankModule` и использует:
|
||
|
||
- `BrokerAccountsService` для проверки счёта;
|
||
- `BrokerPortfolioService` или внутренний shared-path для получения текущих позиций;
|
||
- `BrokerInstrumentsService` для сопоставления T-Bank instrument metadata;
|
||
- `MoexClientService` для дивидендов и bond enrichment;
|
||
- `BrokerOperationsService` для получения фактических прошедших выплат;
|
||
- `CacheService` для кэширования результата по `accountId + from + to + types`.
|
||
|
||
Сервис не записывает события в Prisma и не вводит отдельные таблицы в первой версии.
|
||
|
||
### Формирование событий
|
||
|
||
По акциям:
|
||
|
||
- для каждой позиции, сопоставимой с MOEX share instrument, читаются dividend events;
|
||
- в выборку попадают только события, чья `registryCloseDate` лежит в диапазоне;
|
||
- `estimatedAmount` рассчитывается как текущий `quantity * payoutPerUnit`, если payout известен.
|
||
|
||
По облигациям:
|
||
|
||
- используется уже доступный enrichment-слой MOEX;
|
||
- `nextCouponDate` даёт событие `coupon`;
|
||
- `matDate` даёт событие `maturity`;
|
||
- `offerDate` даёт событие `offer`;
|
||
- `couponValue` и `faceValue` используются для оценки `estimatedAmount`, когда они доступны.
|
||
|
||
### Семантика summary
|
||
|
||
`summary` агрегирует денежные события раздельно по источнику:
|
||
|
||
- дивиденды;
|
||
- купоны;
|
||
- погашения.
|
||
|
||
Поля прогноза считаются только по `source: 'forecast'`, поля факта — только по `source: 'actual'`.
|
||
Оферты остаются в общем списке событий, но не входят в денежные итоги.
|
||
|
||
### Фактические прошедшие события
|
||
|
||
Для прошедшей части диапазона backend читает исполненные операции T-Bank:
|
||
|
||
- `OPERATION_TYPE_DIVIDEND` и `OPERATION_TYPE_DIV_EXT` → `dividend`;
|
||
- `OPERATION_TYPE_COUPON` → `coupon`;
|
||
- `OPERATION_TYPE_BOND_REPAYMENT` и `OPERATION_TYPE_BOND_REPAYMENT_FULL` → `maturity`.
|
||
|
||
Фактическая строка получает `source: 'actual'`, `actualAmount` из `operation.payment`, дату операции
|
||
как `eventDate`, `estimateMode: null`. Прогнозная строка получает `source: 'forecast'`,
|
||
`estimatedAmount`, `estimateMode: 'current_position'`.
|
||
|
||
### Частичная деградация
|
||
|
||
Сервис работает по принципу best-effort:
|
||
|
||
- ошибка по одной бумаге не роняет весь ответ;
|
||
- unsupported или несопоставимые позиции пропускаются;
|
||
- если сумма события не может быть рассчитана, событие всё равно возвращается без суммы.
|
||
|
||
## API и типы
|
||
|
||
### Query contract
|
||
|
||
Новый query DTO:
|
||
|
||
```ts
|
||
type BrokerEventsQuery = {
|
||
from: string;
|
||
to: string;
|
||
types?: string;
|
||
};
|
||
```
|
||
|
||
Обе даты обязательны. `types` опционален: если параметр отсутствует, backend использует все типы.
|
||
|
||
### Response contract
|
||
|
||
```ts
|
||
type BrokerPortfolioEvent = {
|
||
id: string;
|
||
type: 'dividend' | 'coupon' | 'maturity' | 'offer';
|
||
source: 'forecast' | 'actual';
|
||
category: 'cashflow' | 'corporate';
|
||
eventDate: string;
|
||
paymentDate: string | null;
|
||
ticker: string | null;
|
||
name: string | null;
|
||
instrumentUid: string | null;
|
||
instrumentType: 'share' | 'bond' | 'other';
|
||
quantitySnapshot: number | null;
|
||
payoutPerUnit: number | null;
|
||
estimatedAmount: number | null;
|
||
actualAmount: number | null;
|
||
currency: string | null;
|
||
estimateMode: 'current_position' | null;
|
||
};
|
||
|
||
type BrokerEventsSummary = {
|
||
eventCount: number;
|
||
nearestEventDate: string | null;
|
||
totalEstimatedCashflow: number;
|
||
actualCashflow: number;
|
||
forecastEstimatedCashflow: number;
|
||
dividendsTotal: number;
|
||
couponsTotal: number;
|
||
principalRepaymentTotal: number;
|
||
actualDividendsTotal: number;
|
||
actualCouponsTotal: number;
|
||
actualPrincipalRepaymentTotal: number;
|
||
};
|
||
```
|
||
|
||
## Карта файлов
|
||
|
||
### Backend
|
||
|
||
- Create `apps/backend/src/modules/tbank/dto/broker-events-query.dto.ts`
|
||
- Create `apps/backend/src/modules/tbank/dto/broker-events-response.dto.ts`
|
||
- Modify `apps/backend/src/modules/tbank/dto/broker-envelope.dto.ts`
|
||
- Modify `apps/backend/src/modules/tbank/types/broker.types.ts`
|
||
- Create `apps/backend/src/modules/tbank/services/broker-events.service.ts`
|
||
- Create `apps/backend/src/modules/tbank/services/broker-events.service.spec.ts`
|
||
- Modify `apps/backend/src/modules/tbank/tbank.controller.ts`
|
||
- Modify `apps/backend/src/modules/tbank/tbank.controller.spec.ts`
|
||
- Modify `apps/backend/src/modules/tbank/tbank.module.ts`
|
||
|
||
### Frontend
|
||
|
||
- Modify `apps/frontend/src/shared/api/responses.ts`
|
||
- Modify `apps/frontend/src/shared/api/index.ts`
|
||
- Modify `apps/frontend/src/shared/api/types.ts` through codegen if Swagger changes are published
|
||
- Create `apps/frontend/src/entities/broker-event/api/brokerEventApi.ts`
|
||
- Create `apps/frontend/src/entities/broker-event/model/useBrokerEvents.ts`
|
||
- Create `apps/frontend/src/entities/broker-event/index.ts`
|
||
- Create `apps/frontend/src/pages/broker-events/ui/BrokerEventsPage.tsx`
|
||
- Create `apps/frontend/src/pages/broker-events/index.ts`
|
||
- Create `apps/frontend/src/widgets/broker-events-overview/ui/BrokerEventsOverview.tsx`
|
||
- Create `apps/frontend/src/widgets/broker-events-overview/index.ts`
|
||
- Modify `apps/frontend/src/widgets/broker-account-layout/ui/BrokerAccountLayout.tsx`
|
||
- Modify `apps/frontend/src/pages/broker-account/ui/BrokerAccountOverviewPage.tsx`
|
||
- Modify `apps/frontend/src/app/routing/AppRoutes.tsx`
|
||
|
||
### Documentation
|
||
|
||
- Modify `apps/docs/docs/backend/tbank-invest.md`
|
||
- Modify `apps/docs/docs/backend/modules.md` if the public module description changes materially
|
||
|
||
## Rollout shape
|
||
|
||
### Overview
|
||
|
||
Overview получает компактный блок ближайших событий:
|
||
|
||
- не более 5 событий;
|
||
- summary по ближайшим выплатам не обязателен, если он перегружает карточку;
|
||
- ссылка ведёт в полный раздел `События`.
|
||
|
||
### Dedicated page
|
||
|
||
Страница `События` включает:
|
||
|
||
- выбор диапазона дат;
|
||
- summary cards;
|
||
- список событий;
|
||
- loading, empty и error states.
|
||
|
||
## Verification strategy
|
||
|
||
- Backend unit tests для event mapping, summary и partial failures.
|
||
- Backend controller tests для query validation и response shape.
|
||
- Frontend hook tests для query lifecycle.
|
||
- Frontend page/component tests для фильтра периода, summary и overview-виджета.
|
||
- Final verification:
|
||
- `npm test -w apps/backend`
|
||
- `npm test -w apps/frontend`
|
||
- `npm run build -w apps/backend`
|
||
- `npm run build -w apps/frontend`
|
||
- `npm run build -w apps/docs` если публичные docs менялись
|
||
- `npm run codegen -w apps/frontend` если Swagger-контракт обновлён
|
||
|
||
### Frontend date range UI
|
||
|
||
Даты и типы хранятся в URL-параметрах `from` / `to` / `types` для возможности поделиться ссылкой.
|
||
На странице — два date input из дизайн-системы и multi-select типов событий. dayjs используется для
|
||
форматирования, валидации и дефолтов.
|
||
|
||
Поток данных:
|
||
|
||
```
|
||
useSearchParams → applied filters → useBrokerEvents(query) → TanStack Query
|
||
local draft state → button "Показать" → setSearchParams(applied filters)
|
||
```
|
||
|
||
Дефолт при первом визите без параметров: today-7d – today+7d и все типы событий. Валидация: to >=
|
||
from, выбран хотя бы один тип. При невалидных фильтрах кнопка `Показать` disabled или показывает
|
||
ошибку, запрос не выполняется. Изменение полей не запускает запрос до применения.
|
||
|
||
## Follow-up implementation tasks
|
||
|
||
### Backend
|
||
|
||
1. Расширить `BrokerEventsQueryDto` параметром `types` с валидацией comma-separated значений.
|
||
2. Расширить `BrokerPortfolioEvent` и Swagger DTO полями `source`, `actualAmount`, nullable
|
||
`estimateMode`.
|
||
3. Расширить `BrokerEventsSummary` раздельными полями факта и прогноза.
|
||
4. Добавить в `BrokerEventsService` фильтрацию типов и cache key с `types`.
|
||
5. Инжектировать `BrokerOperationsService` в `BrokerEventsService` и строить actual events по
|
||
исполненным операциям для прошедшей части диапазона.
|
||
6. Исключить дубли forecast/actual для прошедших cashflow-событий по ключу `type + ticker + date`.
|
||
7. Обновить backend unit/controller tests на query `types`, actual events и summary.
|
||
|
||
### Frontend
|
||
|
||
1. Расширить handwritten response types и `BrokerEventsQuery` параметром `types`.
|
||
2. Обновить `useBrokerEvents` query key с учётом `types`.
|
||
3. Переделать `BrokerEventsPage` на applied filters + draft filters + кнопку `Показать`.
|
||
4. Добавить multi-select типов событий по дизайн-системе/текущим UI-паттернам проекта.
|
||
5. Отобразить `Факт`/`Прогноз`, `Поступило`, зелёное выделение actual-сумм и раздельный summary.
|
||
6. Обновить frontend tests на отсутствие запроса при черновом изменении фильтров, применение по
|
||
кнопке, multi-select типов и actual event styling.
|