128 lines
4.3 KiB
TypeScript
128 lines
4.3 KiB
TypeScript
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<Map<string, Partial<TBankInstrument>>> {
|
|
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),
|
|
);
|
|
}
|
|
}
|