- 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
128 lines
3.9 KiB
TypeScript
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 },
|
|
])
|
|
})
|
|
})
|