From 09213624c3ef001e041b2708d405d493ff50b968 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sat, 20 Jun 2026 12:49:04 +0300 Subject: [PATCH] test: remove legacy broker position duplicates --- .../src/pages/broker/brokerAllocation.test.ts | 144 ----------- .../src/pages/broker/brokerDisplay.test.ts | 227 ------------------ 2 files changed, 371 deletions(-) delete mode 100644 apps/frontend/src/pages/broker/brokerAllocation.test.ts delete mode 100644 apps/frontend/src/pages/broker/brokerDisplay.test.ts diff --git a/apps/frontend/src/pages/broker/brokerAllocation.test.ts b/apps/frontend/src/pages/broker/brokerAllocation.test.ts deleted file mode 100644 index ce66737..0000000 --- a/apps/frontend/src/pages/broker/brokerAllocation.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import type { BrokerMoney, BrokerPortfolio } from '../../api/responses'; -import { buildBrokerAllocation } from './brokerAllocation'; - -function money(value: number): BrokerMoney { - return { - currency: 'RUB', - units: String(Math.trunc(value)), - nano: 0, - value, - }; -} - -function portfolio( - values: Partial>, -): BrokerPortfolio { - const total = (key: keyof typeof values): BrokerMoney | null => { - const value = values[key]; - return value == null ? null : money(value); - }; - - return { - account: { - id: 'acc-1', - type: 'brokerage', - name: 'Основной', - status: 'open', - openedAt: '2024-01-01T00:00:00.000Z', - accessLevel: 'full_access', - }, - positionCounts: { - shares: 1, - bonds: 1, - etf: 1, - other: 0, - }, - totals: { - shares: total('shares'), - bonds: total('bonds'), - etf: total('etf'), - currencies: total('currencies'), - futures: null, - options: null, - structuredProducts: null, - dfa: null, - portfolio: total('portfolio'), - }, - yields: { - expectedPercent: null, - daily: null, - dailyPercent: null, - }, - cash: [], - blockedCash: [], - asOf: '2025-01-01T00:00:00.000Z', - }; -} - -describe('buildBrokerAllocation', () => { - it('builds allocation sectors in display order', () => { - expect( - buildBrokerAllocation( - portfolio({ shares: 400, bonds: 300, etf: 100, currencies: 150, portfolio: 1000 }), - ), - ).toEqual({ - total: 1000, - sectors: [ - { key: 'shares', label: 'Акции', value: 400, percent: 40, color: '#4969f5' }, - { key: 'bonds', label: 'Облигации', value: 300, percent: 30, color: '#e5a33c' }, - { key: 'etf', label: 'ETF/фонды', value: 100, percent: 10, color: '#62b889' }, - { key: 'cash', label: 'Деньги', value: 150, percent: 15, color: '#7b63cf' }, - { key: 'other', label: 'Прочие', value: 50, percent: 5, color: '#aeb6c5' }, - ], - negative: [], - }); - }); - - it('omits zero-value sectors', () => { - const result = buildBrokerAllocation( - portfolio({ shares: 600, bonds: 0, etf: null, currencies: 400, portfolio: 1000 }), - ); - - expect(result.sectors.map(({ key }) => key)).toEqual(['shares', 'cash']); - expect(result.negative).toEqual([]); - }); - - it('reports a negative residual outside the sectors', () => { - const result = buildBrokerAllocation( - portfolio({ shares: 700, bonds: 300, etf: 100, currencies: 50, portfolio: 1000 }), - ); - - expect(result.sectors.map(({ key }) => key)).toEqual(['shares', 'bonds', 'etf', 'cash']); - expect(result.negative).toEqual([ - { key: 'other', label: 'Прочие', value: -150, color: '#aeb6c5' }, - ]); - }); - - it('ignores a tiny negative residual caused by decimal arithmetic', () => { - const result = buildBrokerAllocation(portfolio({ shares: 0.1, bonds: 0.2, portfolio: 0.3 })); - - expect(result.sectors.map(({ key }) => key)).toEqual(['shares', 'bonds']); - expect(result.negative).toEqual([]); - }); - - it('ignores a tiny positive residual caused by decimal arithmetic', () => { - const result = buildBrokerAllocation( - portfolio({ shares: 0.3, portfolio: 0.30000000000000004 }), - ); - - expect(result.sectors.map(({ key }) => key)).toEqual(['shares']); - expect(result.negative).toEqual([]); - }); - - it('returns no allocation for missing or nonpositive portfolio totals', () => { - expect(buildBrokerAllocation(portfolio({ shares: 100, portfolio: null }))).toEqual({ - total: 0, - sectors: [], - negative: [], - }); - expect(buildBrokerAllocation(portfolio({ shares: 100, portfolio: 0 }))).toEqual({ - total: 0, - sectors: [], - negative: [], - }); - expect(buildBrokerAllocation(portfolio({ shares: 100, portfolio: -10 }))).toEqual({ - total: -10, - sectors: [], - negative: [], - }); - }); - - it('preserves named negative components when the portfolio total is nonpositive', () => { - expect(buildBrokerAllocation(portfolio({ shares: 100, bonds: -20, portfolio: 0 }))).toEqual({ - total: 0, - sectors: [], - negative: [{ key: 'bonds', label: 'Облигации', value: -20, color: '#e5a33c' }], - }); - expect(buildBrokerAllocation(portfolio({ currencies: -30, etf: 5, portfolio: -10 }))).toEqual({ - total: -10, - sectors: [], - negative: [{ key: 'cash', label: 'Деньги', value: -30, color: '#7b63cf' }], - }); - }); -}); diff --git a/apps/frontend/src/pages/broker/brokerDisplay.test.ts b/apps/frontend/src/pages/broker/brokerDisplay.test.ts deleted file mode 100644 index 0d7b808..0000000 --- a/apps/frontend/src/pages/broker/brokerDisplay.test.ts +++ /dev/null @@ -1,227 +0,0 @@ -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'); - }); -});