228 lines
7.2 KiB
TypeScript
228 lines
7.2 KiB
TypeScript
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>): 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>): 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');
|
|
});
|
|
});
|