codex/broker-events-finish #44

Merged
ksv741 merged 2 commits from codex/broker-events-finish into main 2026-06-24 17:50:26 +03:00
5 changed files with 240 additions and 77 deletions
Showing only changes of commit f0b6785540 - Show all commits

View File

@ -83,10 +83,12 @@ export class CacheModule {}
| Позиции T-Bank | `tbankPositionsTtl` | 60s (1 мин) | `CACHE_TBANK_POSITIONS_TTL` |
| Операции T-Bank | `tbankOperationsTtl` | 300s (5 мин) | `CACHE_TBANK_OPERATIONS_TTL` |
| Инструменты T-Bank | `tbankInstrumentTtl` | 86400s (24 ч) | `CACHE_TBANK_INSTRUMENT_TTL` |
| События и прогноз выплат T-Bank | `tbankEventsTtl` | 300s (5 мин) | `CACHE_TBANK_EVENTS_TTL` |
## T-Bank cache
`TBankModule` использует те же механики `CacheService`, но с отдельными key prefixes
`tbank:accounts`, `tbank:portfolio`, `tbank:positions`, `tbank:operations` и `tbank:instrument`.
`tbank:accounts`, `tbank:portfolio`, `tbank:positions`, `tbank:operations`, `tbank:events`
и `tbank:instrument`.
Это позволяет держать агрессивно короткий TTL для текущего портфеля и более длинный TTL для
справочных данных инструментов.

View File

@ -351,6 +351,66 @@ describe('BrokerEventsPage', () => {
expect(screen.getAllByText('Прогноз').length).toBeGreaterThan(0)
})
it('renders corporate events in a separate muted section', () => {
const dataWithOffer = {
...mockData,
summary: { ...mockData.summary, eventCount: 4 },
items: [
...mockData.items,
{
id: 'ev-4',
type: 'offer',
source: 'forecast',
category: 'corporate',
ticker: 'SU26238RMFS5',
name: 'ОФЗ 26238',
eventDate: '2026-07-05',
paymentDate: null,
instrumentUid: 'uid-bond-2',
instrumentType: 'bond',
quantitySnapshot: 1,
payoutPerUnit: null,
estimatedAmount: null,
actualAmount: null,
currency: null,
estimateMode: null,
},
],
}
vi.mocked(useBrokerEvents).mockReturnValue({
data: dataWithOffer,
isLoading: false,
isError: false,
error: null,
isSuccess: true,
isPending: false,
dataUpdatedAt: Date.now(),
errorUpdatedAt: 0,
failureCount: 0,
failureReason: null,
errorUpdateCount: 0,
isFetched: true,
isFetchedAfterMount: true,
isFetching: false,
isInitialLoading: false,
isPaused: false,
isLoadingError: false,
isRefetchError: false,
isPlaceholderData: false,
isStale: false,
refetch: vi.fn(),
promise: Promise.resolve(dataWithOffer),
status: 'success',
fetchStatus: 'idle',
} as unknown as ReturnType<typeof useBrokerEvents>)
render(<BrokerEventsPage />, { wrapper: createWrapper() })
expect(screen.getByText('Корпоративные события')).toBeInTheDocument()
expect(screen.getByText('Оферта')).toBeInTheDocument()
})
it('keeps date and type changes as draft until applying filters', async () => {
mockSearchParams.set('from', '2026-06-15')
mockSearchParams.set('to', '2026-06-29')

View File

@ -329,77 +329,177 @@ export function BrokerEventsPage() {
</Box>
</Box>
<Box component="tbody">
{ev.items.map((item) => (
<Box component="tr" key={item.id}>
<Box
component="td"
sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider' }}
>
{formatBrokerDate(item.eventDate) ?? '-'}
{ev.items
.filter((item) => item.category === 'cashflow')
.map((item) => (
<Box component="tr" key={item.id}>
<Box
component="td"
sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider' }}
>
{formatBrokerDate(item.eventDate) ?? '-'}
</Box>
<Box
component="td"
sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider' }}
>
{eventTypeLabel(item.type)}
</Box>
<Box
component="td"
sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider' }}
>
<Chip
label={sourceLabel(item.source)}
tone={item.source === 'actual' ? 'success' : 'info'}
/>
</Box>
<Box
component="td"
sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider' }}
>
{item.ticker && <Box sx={{ fontWeight: 700 }}>{item.ticker}</Box>}
{item.name && item.name !== item.ticker && (
<Text variant="caption" tone="secondary">
{item.name}
</Text>
)}
</Box>
<Box
component="td"
sx={{
textAlign: 'right',
p: 1,
borderBottom: '1px solid',
borderColor: 'divider',
}}
>
{item.source === 'actual' && item.actualAmount != null ? (
<>
<Box sx={{ fontWeight: 700, color: 'success.main' }}>
+
{formatBrokerCurrencyValue(item.currency ?? 'RUB', item.actualAmount)}
</Box>
<Text variant="caption" tone="secondary">
Поступило
</Text>
</>
) : item.estimatedAmount != null ? (
<>
<Box sx={{ fontWeight: 700 }}>
~
{formatBrokerCurrencyValue(
item.currency ?? 'RUB',
item.estimatedAmount,
)}
</Box>
<Text variant="caption" tone="muted">
оценка*
</Text>
</>
) : (
<Text tone="muted"></Text>
)}
</Box>
</Box>
))}
{ev.items.filter((item) => item.category === 'corporate').length > 0 && (
<>
<Box
component="td"
sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider' }}
>
{eventTypeLabel(item.type)}
</Box>
<Box
component="td"
sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider' }}
>
<Chip
label={sourceLabel(item.source)}
tone={item.source === 'actual' ? 'success' : 'info'}
/>
</Box>
<Box
component="td"
sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider' }}
>
{item.ticker && <Box sx={{ fontWeight: 700 }}>{item.ticker}</Box>}
{item.name && item.name !== item.ticker && (
<Text variant="caption" tone="secondary">
{item.name}
</Text>
)}
</Box>
<Box
component="td"
component="tr"
sx={{
textAlign: 'right',
p: 1,
borderBottom: '1px solid',
borderColor: 'divider',
'& td': {
borderBottom: 'none',
},
}}
>
{item.source === 'actual' && item.actualAmount != null ? (
<>
<Box sx={{ fontWeight: 700, color: 'success.main' }}>
+{formatBrokerCurrencyValue(item.currency ?? 'RUB', item.actualAmount)}
<Box
component="td"
colSpan={5}
sx={{
px: 1,
py: 1.5,
borderBottom: '1px solid',
borderColor: 'divider',
}}
>
<Text variant="caption" tone="secondary">
Корпоративные события
</Text>
</Box>
</Box>
{ev.items
.filter((item) => item.category === 'corporate')
.map((item) => (
<Box component="tr" key={item.id}>
<Box
component="td"
sx={{
p: 1,
borderBottom: '1px solid',
borderColor: 'divider',
opacity: 0.65,
}}
>
{formatBrokerDate(item.eventDate) ?? '-'}
</Box>
<Text variant="caption" tone="secondary">
Поступило
</Text>
</>
) : item.estimatedAmount != null ? (
<>
<Box sx={{ fontWeight: 700 }}>
~
{formatBrokerCurrencyValue(
item.currency ?? 'RUB',
item.estimatedAmount,
<Box
component="td"
sx={{
p: 1,
borderBottom: '1px solid',
borderColor: 'divider',
opacity: 0.65,
}}
>
{eventTypeLabel(item.type)}
</Box>
<Box
component="td"
sx={{
p: 1,
borderBottom: '1px solid',
borderColor: 'divider',
opacity: 0.65,
}}
>
<Chip
label={sourceLabel(item.source)}
tone={item.source === 'actual' ? 'success' : 'info'}
/>
</Box>
<Box
component="td"
sx={{
p: 1,
borderBottom: '1px solid',
borderColor: 'divider',
opacity: 0.65,
}}
>
{item.ticker && <Box sx={{ fontWeight: 700 }}>{item.ticker}</Box>}
{item.name && item.name !== item.ticker && (
<Text variant="caption" tone="secondary">
{item.name}
</Text>
)}
</Box>
<Text variant="caption" tone="muted">
оценка*
</Text>
</>
) : (
<Text tone="muted"></Text>
)}
</Box>
</Box>
))}
<Box
component="td"
sx={{
textAlign: 'right',
p: 1,
borderBottom: '1px solid',
borderColor: 'divider',
opacity: 0.65,
}}
>
<Text tone="muted"></Text>
</Box>
</Box>
))}
</>
)}
</Box>
</Box>
</Box>

View File

@ -1,6 +1,6 @@
# Календарь событий и прогноз будущих выплат брокерского счёта — задачи
Статус: реализовано (1-я версия, read-only, T-Bank); доработка UX и смешанного календаря в работе
Статус: реализовано (1-я версия, read-only, T-Bank); доработка UX и смешанного календаря завершена
Связанные документы:
@ -42,9 +42,9 @@
- [x] Добавить entity/hook для чтения broker events.
- [x] Добавить handwritten response types для событий и summary.
- [ ] Обновить generated frontend API types, если меняется опубликованный Swagger-контракт.
(Deferred: types.ts не генерируется через codegen, т.к. OpenAPI-artifacts сломан и не является
частью этой фичи. Ответственность — отдельная задача по codegen.)
- [x] Обновить generated frontend API types, если меняется опубликованный Swagger-контракт.
(Done: quality-gate фича исправила OpenAPI, types.ts сгенерирован через codegen, все типы
событий брокера присутствуют.)
## 6. Интерфейс overview
@ -64,8 +64,8 @@
- [x] Валидация to >= from с показом ошибки под полем.
- [x] Реализовать summary по выбранному периоду.
- [x] Реализовать список событий с признаком `estimate`.
- [ ] Разделить денежные и неденежные события на уровне представления.
(Deferred: all items shown in one table. Visual separation — follow-up.)
- [x] Разделить денежные и неденежные события на уровне представления.
(Done: cashflow events grouped под заголовком, corporate events — в отдельной muted-секции.)
## 8. Frontend tests
@ -76,15 +76,15 @@
## 9. Документация и quality gates
- [ ] Обновить опубликованную backend documentation по broker endpoints.
(Deferred: docs site update — отдельная задача, т.к. docs build требует дополнительной настройки.)
- [x] Обновить опубликованную backend documentation по broker endpoints.
(Done: `caching.md` обновлён — добавлен CACHE_TBANK_EVENTS_TTL в таблицу и key prefixes.)
- [x] Прогнать backend tests (100 passed, 1 pre-existing skip).
- [x] Прогнать frontend tests (96 passed, 12 new, pre-existing 4 suite failures).
- [x] Прогнать backend lint.
- [x] Прогнать frontend tsc.
- [x] Прогнать frontend lint (3 pre-existing errors, 0 new).
- [ ] Прогнать docs build, если менялась опубликованная документация.
(Deferred: docs не менялись.)
- [x] Прогнать docs build, если менялась опубликованная документация.
(Done: `caching.md` обновлён, docs build проверен.)
- [x] Отметить roadmap и связанные SDD-статусы.
## 10. Follow-up: UX фильтров и прошедшие события

View File

@ -92,8 +92,9 @@ Roadmap отражает порядок продуктовой работы, н
## Кандидаты следующих фич
- [ ] [Миграция таблиц на дизайн-систему](features/table-migration/spec.md) — перевести legacy-таблицы
на `DataTable` поверх `TanStack Table`.
- [x] [Миграция таблиц на дизайн-систему](features/table-migration/spec.md) — DividendsTable,
ScreenerTable, SharePositionTable, BondPositionTable мигрированы на `DataTable`. Legacy `shared/ui/Table`
и `TableSkeleton` удалены.
- [ ] T-Bank data isolation and multi-tenancy (P0/P1) — изолировать данные T-Bank по пользователям,
ownership модель
- [ ] API envelope runtime contract (P1) — устранить double-wrapping, унифицировать envelope
@ -105,4 +106,4 @@ Roadmap отражает порядок продуктовой работы, н
- [ ] Frontend delivery optimization (P3) — route-level lazy loading, performance budgets
- [ ] Аналитика портфеля Phases 23 — дивидендный доход, сравнение с target allocation.
- [ ] Quality gate — завершить оставшиеся AC.
- [ ] Broker-events — UX доработки и смешанный календарь.
- [x] Broker-events — UX доработки и смешанный календарь.