moex-vibe/apps/frontend/src/entities/broker-account/model/brokerAccountsOverview.test.ts
Sergey Krylov 9932278b64 feat: complete Phase 3 API type unification, delete stale test file
- Delete shared/api/responses.ts, move type re-exports to index.ts
- Update all 55+ imports from shared/api/responses to shared/api
- Delete stale client.test.ts (tested deleted client.ts)
- Run biome checks and fix import ordering
- Update tasks.md and plan.md to reflect actual approach
2026-06-23 20:24:16 +03:00

128 lines
3.9 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import type { BrokerPortfolio } from '@/shared/api'
import { aggregateBrokerAccounts } from '../model/brokerAccountsOverview'
function portfolio(
id: string,
currency: string,
total: number,
daily: number | null,
cash: number,
): BrokerPortfolio {
return {
account: {
id,
type: 'brokerage',
name: id,
status: 'ACCOUNT_STATUS_OPEN',
openedAt: '2022-06-16T00:00:00.000Z',
accessLevel: null,
},
positionCounts: { shares: 1, bonds: 1, etf: 0, other: 0 },
totals: {
shares: { currency, units: '0', nano: 0, value: total * 0.5 },
bonds: { currency, units: '0', nano: 0, value: total * 0.3 },
etf: null,
currencies: { currency, units: '0', nano: 0, value: total * 0.2 },
futures: null,
options: null,
structuredProducts: null,
dfa: null,
portfolio: { currency, units: '0', nano: 0, value: total },
},
yields: {
expectedPercent: 10,
daily: daily === null ? null : { currency, units: '0', nano: 0, value: daily },
dailyPercent: null,
},
cash: [{ currency, units: '0', nano: 0, value: cash }],
blockedCash: [],
asOf: '2026-06-19T10:00:00.000Z',
}
}
describe('aggregateBrokerAccounts', () => {
it('sums comparable portfolios and uses the specified daily percent formula', () => {
const result = aggregateBrokerAccounts([
portfolio('a', 'RUB', 1_100, 100, 200),
portfolio('b', 'RUB', 2_200, 200, 300),
])
expect(result.portfolios).toEqual([
expect.objectContaining({
currency: 'RUB',
total: 3_300,
daily: 300,
dailyPercent: 10,
allocation: { shares: 1_650, bonds: 990, etf: 0, cash: 660, other: 0 },
}),
])
expect(result.cash).toEqual([{ currency: 'RUB', value: 500 }])
})
it('keeps different currencies separate', () => {
const result = aggregateBrokerAccounts([
portfolio('rub', 'RUB', 1_100, 100, 200),
portfolio('usd', 'USD', 550, 50, 25),
])
expect(result.portfolios.map(({ currency, total }) => ({ currency, total }))).toEqual([
{ currency: 'RUB', total: 1_100 },
{ currency: 'USD', total: 550 },
])
})
it('does not expose a daily percent when one account lacks daily data', () => {
const result = aggregateBrokerAccounts([
portfolio('a', 'RUB', 1_100, 100, 200),
portfolio('b', 'RUB', 2_000, null, 300),
])
expect(result.portfolios[0]).toMatchObject({ daily: null, dailyPercent: null })
})
it('does not expose a daily percent when start of day is non-positive', () => {
const result = aggregateBrokerAccounts([portfolio('a', 'RUB', 100, 100, 20)])
expect(result.portfolios[0]).toMatchObject({ daily: 100, dailyPercent: null })
})
it('clamps negative residual other allocation to zero', () => {
const overAllocated = portfolio('a', 'RUB', 1_000, 50, 100)
overAllocated.totals.shares!.value = 700
overAllocated.totals.bonds!.value = 400
overAllocated.totals.currencies!.value = 100
const result = aggregateBrokerAccounts([overAllocated])
expect(result.portfolios[0].allocation).toEqual({
shares: 700,
bonds: 400,
etf: 0,
cash: 100,
other: 0,
})
})
it('returns empty summaries for empty or unsupported portfolios', () => {
const missingTotal = portfolio('a', 'RUB', 1_000, 50, 100)
missingTotal.totals.portfolio = null
expect(aggregateBrokerAccounts([])).toEqual({ portfolios: [], cash: [] })
expect(aggregateBrokerAccounts([missingTotal])).toEqual({
portfolios: [],
cash: [{ currency: 'RUB', value: 100 }],
})
})
it('groups cash separately by currency', () => {
const mixedCash = portfolio('a', 'RUB', 1_000, 50, 100)
mixedCash.cash.push({ currency: 'USD', units: '0', nano: 0, value: 25 })
expect(aggregateBrokerAccounts([mixedCash]).cash).toEqual([
{ currency: 'RUB', value: 100 },
{ currency: 'USD', value: 25 },
])
})
})