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 }, ]) }) })