refactor: organize backend by business responsibility
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import { portfolioStructure } from './portfolio-structure';
|
||||
import { PortfolioPositionDto } from '../presentation/portfolio.dto';
|
||||
|
||||
const position = (
|
||||
type: string,
|
||||
value: string | null,
|
||||
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);
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user