import { PortfolioPositionDto } from '../presentation/portfolio.dto'; import { portfolioStructure } from './portfolio-structure'; const position = ( type: string, value: null | string, currency = 'RUB', ): PortfolioPositionDto => ({ type, currentValue: value === null ? null : { currency, value }, quantity: '1', displayName: 'Fixture', figi: null, isin: null, ticker: null, logoName: null, absoluteReturn: null, averagePurchasePrice: null, }); describe('portfolio structure', () => { it('includes cash once and keeps currency instruments among other assets', () => { const result = portfolioStructure( [ position('STOCK', '60'), position('BOND', '20'), position('CURRENCY', '10'), ], [{ currency: 'RUB', value: '10' }], ); expect(result.currencies[0]).toEqual({ currency: 'RUB', totalValue: '100', hasNegativeValues: false, categories: [ { type: 'STOCK', value: '60', sharePercent: '60' }, { type: 'BOND', value: '20', sharePercent: '20' }, { type: 'OTHER', value: '10', sharePercent: '10' }, { type: 'CASH', value: '10', sharePercent: '10' }, ], }); }); it('sums beyond Number precision and separates currencies without FX', () => { const result = portfolioStructure( [ position('STOCK', '9007199254740993.000000001'), position('STOCK', '0.000000009'), position('BOND', '2', 'USD'), ], [{ currency: 'USD', value: '1' }], ); expect(result.currencies[0].totalValue).toBe('9007199254740993.00000001'); expect(result.currencies[1].totalValue).toBe('3'); expect(result.currencies[1].categories[1].sharePercent).toBe('66.666667'); expect(result.currencies[1].categories[3].sharePercent).toBe('33.333333'); }); it('represents cash-only, empty and zero-valued portfolios', () => { expect(portfolioStructure([], []).currencies).toEqual([]); expect( portfolioStructure([], [{ currency: 'EUR', value: '10' }]).currencies[0] .categories[3].sharePercent, ).toBe('100'); expect( portfolioStructure( [position('STOCK', '0')], [], ).currencies[0].categories.every((item) => item.sharePercent === null), ).toBe(true); }); it('retains unvalued categories without assigning zero or guessing a currency', () => { const result = portfolioStructure( [position('BOND', null), position('BOND', null), position('STOCK', '50')], [], ); expect(result.unvaluedCategories).toEqual([{ type: 'BOND', count: 2 }]); expect(result.currencies[0].totalValue).toBe('50'); expect( result.currencies[0].categories.every( (item) => item.sharePercent === null, ), ).toBe(true); expect( portfolioStructure([position('OTHER', null)], []).currencies, ).toEqual([]); }); it.each([ [[position('STOCK', '-20'), position('STOCK', '100')], []], [ [], [ { currency: 'RUB', value: '-20' }, { currency: 'RUB', value: '100' }, ], ], [[position('BOND', '-20')], []], [[position('BOND', '-20'), position('BOND', '20')], []], ])( 'preserves negative constituents even when their category nets positive or zero', (positions, cash) => { const result = portfolioStructure(positions, cash); expect(result.currencies[0].hasNegativeValues).toBe(true); expect( result.currencies[0].categories.every( (item) => item.sharePercent === null, ), ).toBe(true); }, ); });