import { Injectable, NotFoundException } from '@nestjs/common'; import { CacheService } from '../../cache/cache.service'; import { mapBrokerPortfolio, mapBrokerPositionsPage } from '../mappers/portfolio.mapper'; import { TBANK_CACHE_KEYS } from '../tbank.config'; import type { BrokerPortfolio, BrokerPositionsPage } from '../types/broker.types'; import type { TBankInstrument, TBankPortfolioResponse, TBankPositionsResponse, } from '../types/tbank-proto.types'; import { BrokerAccountsService } from './broker-accounts.service'; import { BrokerInstrumentsService } from './broker-instruments.service'; import { TBankClientService } from './tbank-client.service'; @Injectable() export class BrokerPortfolioService { constructor( private readonly accountsService: BrokerAccountsService, private readonly instrumentsService: BrokerInstrumentsService, private readonly tbankClient: TBankClientService, private readonly cacheService: CacheService, ) {} async getPortfolio(accountId: string): Promise<{ data: BrokerPortfolio; meta: { fromCache: boolean; cachedAt: string | null }; }> { const account = await this.accountsService.findById(accountId); if (!account) throw new NotFoundException('Broker account not found'); const result = await this.cacheService.getOrFetch( TBANK_CACHE_KEYS.portfolio, [accountId], async () => { const operationsClient = this.tbankClient.getServiceClient('OperationsService') as any; const [portfolio, positions] = await Promise.all([ this.tbankClient.callUnary< { accountId: string; currency: string }, TBankPortfolioResponse >( 'OperationsService/GetPortfolio', operationsClient.getPortfolio.bind(operationsClient), { accountId, currency: 'RUB' }, ), this.tbankClient.callUnary<{ accountId: string }, TBankPositionsResponse>( 'OperationsService/GetPositions', operationsClient.getPositions.bind(operationsClient), { accountId }, ), ]); const instrumentMap = await this.buildInstrumentMap(portfolio); return mapBrokerPortfolio({ account, portfolio, positions, instruments: instrumentMap }); }, 'tbankPortfolioTtl', ); return { data: result.data, meta: { fromCache: result.fromCache, cachedAt: result.cachedAt }, }; } async getPositions( accountId: string, cursor?: string, limit = 10, ): Promise<{ data: BrokerPositionsPage; meta: { fromCache: boolean; cachedAt: string | null }; }> { const account = await this.accountsService.findById(accountId); if (!account) throw new NotFoundException('Broker account not found'); const result = await this.cacheService.getOrFetch( TBANK_CACHE_KEYS.positions, [accountId, cursor ?? '', String(limit)], async () => { const operationsClient = this.tbankClient.getServiceClient('OperationsService') as any; const portfolio = await this.tbankClient.callUnary< { accountId: string; currency: string }, TBankPortfolioResponse >('OperationsService/GetPortfolio', operationsClient.getPortfolio.bind(operationsClient), { accountId, currency: 'RUB', }); const instrumentMap = await this.buildInstrumentMap(portfolio); return mapBrokerPositionsPage({ accountId, portfolio, instruments: instrumentMap, cursor, limit, }); }, 'tbankPositionsTtl', ); return { data: result.data, meta: { fromCache: result.fromCache, cachedAt: result.cachedAt }, }; } private async buildInstrumentMap( portfolio: TBankPortfolioResponse, ): Promise>> { const ids = Array.from( new Set( (portfolio.positions ?? []).map((position) => position.instrumentUid).filter(Boolean), ), ) as string[]; const results = await Promise.allSettled( ids.map(async (id) => [id, await this.instrumentsService.findByInstrumentUid(id)] as const), ); const entries = results.flatMap((result) => result.status === 'fulfilled' ? [result.value] : [], ); return new Map( entries.filter((entry): entry is readonly [string, TBankInstrument] => entry[1] !== null), ); } }