import { describe, expect, it } from 'vitest'; import type { BrokerOperation, BrokerPosition } from '../../api/responses'; import { BROKER_OPERATION_TYPE_OPTIONS, getBrokerInstrumentPath, getBrokerOperationImpact, getBrokerOperationTypeLabel, getBrokerPositionGroup, isBrokerOperationType, } from './brokerDisplay'; function position(input: Partial): BrokerPosition { return { figi: null, instrumentUid: null, positionUid: null, ticker: null, classCode: null, instrumentType: null, name: null, quantity: null, blockedLots: null, currentPrice: null, currentValue: null, averagePositionPrice: null, expectedYieldPercent: null, dailyYield: null, ...input, }; } function operation(input: Partial): BrokerOperation { return { cursor: null, accountId: 'acc-1', id: null, parentOperationId: null, date: null, type: 'OPERATION_TYPE_UNSPECIFIED', category: 'other', description: null, name: null, state: null, instrumentUid: null, figi: null, ticker: null, classCode: null, instrumentType: null, payment: null, price: null, commission: null, yield: null, accruedInt: null, quantity: null, quantityDone: null, ...input, }; } describe('broker display helpers', () => { it('groups positions by instrument type', () => { expect(getBrokerPositionGroup(position({ instrumentType: 'share' }))).toBe('shares'); expect(getBrokerPositionGroup(position({ instrumentType: 'bond' }))).toBe('bonds'); expect(getBrokerPositionGroup(position({ instrumentType: 'etf' }))).toBe('other'); expect(getBrokerPositionGroup(position({ instrumentType: null }))).toBe('other'); }); it('builds stock and bond routes from instrument metadata', () => { expect( getBrokerInstrumentPath({ ticker: 'sber', instrumentType: 'share', classCode: 'TQBR' }), ).toBe('/stocks/SBER'); expect( getBrokerInstrumentPath({ ticker: 'SU26238RMFS5', instrumentType: 'bond', classCode: 'TQOB', }), ).toBe('/bonds/SU26238RMFS5'); expect( getBrokerInstrumentPath({ ticker: null, instrumentType: 'share', classCode: 'TQBR' }), ).toBeNull(); expect( getBrokerInstrumentPath({ ticker: 'TMOS', instrumentType: 'etf', classCode: 'TQTF' }), ).toBeNull(); }); it('uses class code fallback when instrument type is missing', () => { expect( getBrokerInstrumentPath({ ticker: 'SBER', instrumentType: null, classCode: 'TQBR' }), ).toBe('/stocks/SBER'); expect( getBrokerInstrumentPath({ ticker: 'RU000A0JX0J2', instrumentType: null, classCode: 'TQOB' }), ).toBe('/bonds/RU000A0JX0J2'); }); it('does not let class code override a known unsupported or conflicting instrument type', () => { expect( getBrokerInstrumentPath({ ticker: 'TMOS', instrumentType: 'etf', classCode: 'TQBR' }), ).toBeNull(); expect( getBrokerInstrumentPath({ ticker: 'SU26238RMFS5', instrumentType: 'bond', classCode: 'TQBR', }), ).toBe('/bonds/SU26238RMFS5'); }); it('maps operation enum values to Russian labels', () => { expect(getBrokerOperationTypeLabel(operation({ type: 'OPERATION_TYPE_COUPON' }))).toBe( 'Выплата купона', ); expect(getBrokerOperationTypeLabel(operation({ type: 'OPERATION_TYPE_TAX' }))).toBe('Налог'); expect(getBrokerOperationTypeLabel(operation({ type: 'OPERATION_TYPE_BUY' }))).toBe('Покупка'); expect( getBrokerOperationTypeLabel( operation({ type: 'OPERATION_TYPE_UNKNOWN_VALUE', description: 'Custom' }), ), ).toBe('Custom'); }); it('exposes independently selectable known operation types', () => { expect(BROKER_OPERATION_TYPE_OPTIONS).toEqual( expect.arrayContaining([ { value: 'OPERATION_TYPE_COUPON', label: 'Выплата купона' }, { value: 'OPERATION_TYPE_TAX', label: 'Налог' }, { value: 'OPERATION_TYPE_BOND_TAX', label: 'Налог по облигациям' }, { value: 'OPERATION_TYPE_DIVIDEND_TAX', label: 'Налог на дивиденды' }, ]), ); }); it('keeps operation type option values unique and labels in Russian order', () => { const values = BROKER_OPERATION_TYPE_OPTIONS.map(({ value }) => value); const labels = BROKER_OPERATION_TYPE_OPTIONS.map(({ label }) => label); expect(new Set(values).size).toBe(values.length); expect(labels).toEqual([...labels].sort((left, right) => left.localeCompare(right, 'ru'))); }); it('keeps operation type options immutable at runtime', () => { expect(Object.isFrozen(BROKER_OPERATION_TYPE_OPTIONS)).toBe(true); expect(BROKER_OPERATION_TYPE_OPTIONS.every((option) => Object.isFrozen(option))).toBe(true); }); it('validates only exact known operation type values', () => { expect(isBrokerOperationType('OPERATION_TYPE_COUPON')).toBe(true); expect(isBrokerOperationType('operation_type_coupon')).toBe(false); expect(isBrokerOperationType('OPERATION_TYPE_UNKNOWN')).toBe(false); expect(isBrokerOperationType(null)).toBe(false); }); it('classifies operations by portfolio impact', () => { expect( getBrokerOperationImpact( operation({ type: 'OPERATION_TYPE_COUPON', category: 'income', payment: { currency: 'RUB', units: '120', nano: 0, value: 120 }, }), ), ).toBe('adds'); expect( getBrokerOperationImpact( operation({ type: 'OPERATION_TYPE_TAX', category: 'tax', payment: { currency: 'RUB', units: '-13', nano: 0, value: -13 }, }), ), ).toBe('reduces'); expect( getBrokerOperationImpact( operation({ type: 'OPERATION_TYPE_SELL', category: 'trade', payment: { currency: 'RUB', units: '1000', nano: 0, value: 1000 }, }), ), ).toBe('neutral'); expect(getBrokerOperationImpact(operation({ type: 'OPERATION_TYPE_UNSPECIFIED' }))).toBe( 'unknown', ); }); it('keeps unknown operation types unclear even when they have non-zero payments', () => { expect( getBrokerOperationImpact( operation({ type: 'OPERATION_TYPE_UNRECOGNIZED_NEW_VALUE', category: 'other', payment: { currency: 'RUB', units: '100', nano: 0, value: 100 }, }), ), ).toBe('unknown'); expect( getBrokerOperationImpact( operation({ type: 'OPERATION_TYPE_UNRECOGNIZED_NEW_VALUE', category: 'other', payment: { currency: 'RUB', units: '-100', nano: 0, value: -100 }, }), ), ).toBe('unknown'); }); it('classifies known income operation types as additions even with weak metadata', () => { expect( getBrokerOperationImpact( operation({ type: 'OPERATION_TYPE_COUPON', category: 'other', payment: null, }), ), ).toBe('adds'); expect( getBrokerOperationImpact( operation({ type: 'OPERATION_TYPE_DIVIDEND', category: 'other', payment: null, }), ), ).toBe('adds'); }); });