From 9b7e5e1215b7c1c5320effb6ba5b2c2a8b0cd604 Mon Sep 17 00:00:00 2001 From: codex-bot Date: Tue, 13 Oct 2026 21:54:51 +0300 Subject: [PATCH] refactor: extract shared public instrument projection (#69) (#84) --- .../application/catalog-discovery.service.ts | 37 +---------- .../application/catalog-enrichment.service.ts | 34 +--------- .../application/public-instrument.spec.ts | 64 +++++++++++++++++++ .../application/public-instrument.ts | 38 +++++++++++ 4 files changed, 104 insertions(+), 69 deletions(-) create mode 100644 src/modules/investments/instruments/application/public-instrument.spec.ts create mode 100644 src/modules/investments/instruments/application/public-instrument.ts diff --git a/src/modules/investments/instruments/application/catalog-discovery.service.ts b/src/modules/investments/instruments/application/catalog-discovery.service.ts index fb3c389..93901be 100644 --- a/src/modules/investments/instruments/application/catalog-discovery.service.ts +++ b/src/modules/investments/instruments/application/catalog-discovery.service.ts @@ -9,9 +9,7 @@ import { StructuredLogger } from '../../../../common/logging/structured-logger.s import { isTbankErrorCode, TbankErrorCode } from '../../../../integrations/tbank/tbank-error-code'; import { TbankAccessService, - type TbankBond, TbankClient, - type TbankShare, tbankSourceTtlMs, type TbankSyncAttempt, } from '../../../../integrations/tbank/tbank.api'; @@ -28,6 +26,7 @@ import { catalogSearchLimit, selectCatalogInstruments, } from './catalog-selection'; +import { publicInstrument } from './public-instrument'; const UNAMBIGUOUS_MATCH_COUNT = 1; const NO_MATCHES = 0; @@ -495,37 +494,3 @@ export class CatalogDiscoveryService { } } } - -function publicInstrument(value: TbankBond | TbankShare) { - const bond = value as TbankBond; - const share = value as TbankShare; - return { - uid: value.uid, - figi: value.figi, - assetUid: value.assetUid, - name: value.name, - ticker: value.ticker, - isin: value.isin, - classCode: value.classCode, - realExchange: value.realExchange, - apiTradeAvailableFlag: value.apiTradeAvailableFlag, - currency: value.currency, - shareType: share.shareType, - sector: value.sector, - lot: value.lot, - brand: value.brand ? { logoName: value.brand.logoName } : undefined, - first_1dayCandleDate: value.first_1dayCandleDate, - nominal: bond.nominal, - initialNominal: bond.initialNominal, - aciValue: bond.aciValue, - maturityDate: bond.maturityDate, - callDate: bond.callDate, - couponQuantityPerYear: bond.couponQuantityPerYear, - floatingCouponFlag: bond.floatingCouponFlag, - perpetualFlag: bond.perpetualFlag, - amortizationFlag: bond.amortizationFlag, - subordinatedFlag: bond.subordinatedFlag, - bondType: bond.bondType, - riskLevel: bond.riskLevel, - }; -} diff --git a/src/modules/investments/instruments/application/catalog-enrichment.service.ts b/src/modules/investments/instruments/application/catalog-enrichment.service.ts index 0c40573..3acb074 100644 --- a/src/modules/investments/instruments/application/catalog-enrichment.service.ts +++ b/src/modules/investments/instruments/application/catalog-enrichment.service.ts @@ -38,6 +38,7 @@ import { import { toSavedCandle } from '../infrastructure/tbank-candle.mapper'; import { catalogSearchLimit } from './catalog-selection'; import { previousClose, timestamp } from './instrument-market'; +import { publicInstrument } from './public-instrument'; const MISSING_DATA_REVISION = 0; const FIRST_QUEUE_INDEX = 0; @@ -651,39 +652,6 @@ export type CatalogPayload = { tradingStatus?: null | string; }; -function publicInstrument(value: TbankBond | TbankShare) { - const bond = value as TbankBond; - const share = value as TbankShare; - return { - uid: value.uid, - figi: value.figi, - assetUid: value.assetUid, - name: value.name, - ticker: value.ticker, - isin: value.isin, - classCode: value.classCode, - realExchange: value.realExchange, - apiTradeAvailableFlag: value.apiTradeAvailableFlag, - currency: value.currency, - shareType: share.shareType, - sector: value.sector, - lot: value.lot, - brand: value.brand ? { logoName: value.brand.logoName } : undefined, - first_1dayCandleDate: value.first_1dayCandleDate, - nominal: bond.nominal, - initialNominal: bond.initialNominal, - aciValue: bond.aciValue, - maturityDate: bond.maturityDate, - callDate: bond.callDate, - couponQuantityPerYear: bond.couponQuantityPerYear, - floatingCouponFlag: bond.floatingCouponFlag, - perpetualFlag: bond.perpetualFlag, - amortizationFlag: bond.amortizationFlag, - subordinatedFlag: bond.subordinatedFlag, - bondType: bond.bondType, - riskLevel: bond.riskLevel, - }; -} function sourcePolicy(kind: CatalogSource['kind']) { if ( [ diff --git a/src/modules/investments/instruments/application/public-instrument.spec.ts b/src/modules/investments/instruments/application/public-instrument.spec.ts new file mode 100644 index 0000000..063ea43 --- /dev/null +++ b/src/modules/investments/instruments/application/public-instrument.spec.ts @@ -0,0 +1,64 @@ +import type { + TbankBond, + TbankShare, +} from '../../../../integrations/tbank/tbank.api'; + +import { publicInstrument } from './public-instrument'; + +const share = { + uid: 'share-uid', + figi: 'FIGI1', + assetUid: 'asset-1', + name: 'Share', + ticker: 'SHAR', + isin: 'US000', + classCode: 'TQBR', + realExchange: 'REAL_EXCHANGE_MOEX', + apiTradeAvailableFlag: true, + currency: 'rub', + shareType: 'SHARE_TYPE_COMMON', + sector: 'Finance', + lot: 1, + brand: { logoName: 'logo.png', title: 'Brand', id: 'brand-1' }, + first_1dayCandleDate: '2020-01-01T00:00:00Z', + privateField: 'must-not-leak', +} as unknown as TbankShare; + +const bond = { + uid: 'bond-uid', + figi: 'FIGI2', + assetUid: 'asset-2', + name: 'Bond', + ticker: 'BOND', + classCode: 'TQOB', + currency: 'rub', + lot: 1, + nominal: { value: '1000', currency: 'rub' }, + initialNominal: { value: '1000', currency: 'rub' }, + aciValue: { value: '10', currency: 'rub' }, + maturityDate: '2030-01-01T00:00:00Z', + couponQuantityPerYear: 2, + bondType: 'BOND_TYPE_EXPIRING', + brand: null, + privateField: 'must-not-leak', +} as unknown as TbankBond; + +describe('publicInstrument projection', () => { + it('keeps approved share fields, trims brand, drops private extras', () => { + const result = publicInstrument(share); + expect(result.uid).toBe('share-uid'); + expect(result.shareType).toBe('SHARE_TYPE_COMMON'); + expect(result.brand).toEqual({ logoName: 'logo.png' }); + expect(result).not.toHaveProperty('privateField'); + expect(result).not.toHaveProperty('brand.title'); + }); + + it('projects bonds with missing optional fields and null brand', () => { + const result = publicInstrument(bond); + expect(result.nominal).toEqual({ value: '1000', currency: 'rub' }); + expect(result.shareType).toBeUndefined(); + expect(result.brand).toBeUndefined(); + expect(result.isin).toBeUndefined(); + expect(result).not.toHaveProperty('privateField'); + }); +}); diff --git a/src/modules/investments/instruments/application/public-instrument.ts b/src/modules/investments/instruments/application/public-instrument.ts new file mode 100644 index 0000000..c35a23b --- /dev/null +++ b/src/modules/investments/instruments/application/public-instrument.ts @@ -0,0 +1,38 @@ +import type { + TbankBond, + TbankShare, +} from '../../../../integrations/tbank/tbank.api'; + +export function publicInstrument(value: TbankBond | TbankShare) { + const bond = value as TbankBond; + const share = value as TbankShare; + return { + uid: value.uid, + figi: value.figi, + assetUid: value.assetUid, + name: value.name, + ticker: value.ticker, + isin: value.isin, + classCode: value.classCode, + realExchange: value.realExchange, + apiTradeAvailableFlag: value.apiTradeAvailableFlag, + currency: value.currency, + shareType: share.shareType, + sector: value.sector, + lot: value.lot, + brand: value.brand ? { logoName: value.brand.logoName } : undefined, + first_1dayCandleDate: value.first_1dayCandleDate, + nominal: bond.nominal, + initialNominal: bond.initialNominal, + aciValue: bond.aciValue, + maturityDate: bond.maturityDate, + callDate: bond.callDate, + couponQuantityPerYear: bond.couponQuantityPerYear, + floatingCouponFlag: bond.floatingCouponFlag, + perpetualFlag: bond.perpetualFlag, + amortizationFlag: bond.amortizationFlag, + subordinatedFlag: bond.subordinatedFlag, + bondType: bond.bondType, + riskLevel: bond.riskLevel, + }; +}