import { formatDecimal } from '#src/common/decimal'; import { candidateTransfers, readPerformanceOperations, sumFlows } from './performance-flows'; const at = (date: string) => new Date(date); const op = (accountId: string, type: string, date: string, units: string, currency = 'rub') => ({ accountId, occurredAt: at(date), instrumentUid: null, payload: { type, payment: { units, nano: 0, currency } }, }); describe('performance cash flows', () => { it('includes only external cash and preserves dividends within the portfolio', () => { const operations = [ op('A', 'OPERATION_TYPE_INPUT', '2025-01-01T15:00:00Z', '100000'), op('A', 'OPERATION_TYPE_INPUT_ACQUIRING', '2025-02-01T15:00:00Z', '50000'), op('A', 'OPERATION_TYPE_OUTPUT', '2025-03-01T15:00:00Z', '-20000'), op('A', 'OPERATION_TYPE_DIVIDEND', '2025-04-01T15:00:00Z', '3000'), ]; const result = readPerformanceOperations(operations); expect(formatDecimal(sumFlows(result.flows))).toBe('130000'); expect(result.flows).toHaveLength(3); }); it('treats equal opposite cash movements as unconfirmed transfer candidates, not a proven pair', () => { const result = readPerformanceOperations([ op('A', 'OPERATION_TYPE_OUTPUT', '2025-03-01T15:00:00Z', '-50000'), op('B', 'OPERATION_TYPE_INPUT', '2025-03-01T15:01:00Z', '50000'), ]); expect(candidateTransfers(result)).toEqual([{ accountIds: ['A', 'B'], type: 'cash' }]); expect(result.flows).toHaveLength(2); }); it('detects unvalued securities transfers', () => { const result = readPerformanceOperations([ op('B', 'OPERATION_TYPE_INPUT_SECURITIES', '2025-01-01T15:00:00Z', '0'), ]); expect([...result.unvaluedAccounts]).toEqual(['B']); }); it('skips non-RUB cash movements read back from persisted payloads', () => { const result = readPerformanceOperations([ op('A', 'OPERATION_TYPE_INPUT', '2025-01-01T15:00:00Z', '100'), op('A', 'OPERATION_TYPE_INPUT', '2025-02-01T15:00:00Z', '21', 'usd'), ]); expect(result.flows).toHaveLength(1); expect(formatDecimal(sumFlows(result.flows))).toBe('100'); expect([...result.missingAmountAccounts]).toEqual(['A']); }); });