- Add DomainException base class extending HttpException - Add EntityNotFoundException, PortfolioAccessDeniedException, MoexApiException, TBankApiException, TBankNotConfiguredException - Update HttpExceptionFilter with unhandled error logging - Replace generic NestJS exceptions in services with domain exceptions - Update all affected tests - 117 tests pass, build succeeds
85 lines
3.1 KiB
TypeScript
85 lines
3.1 KiB
TypeScript
import { Injectable } from '@nestjs/common';
|
|
import { CacheService } from '../../cache/cache.service';
|
|
import { ApiEnvelopePayload } from '../../../common/dto/api-response.dto';
|
|
import type { BrokerOperationQueryDto } from '../dto/broker-operation-query.dto';
|
|
import { mapOperationsPage } from '../mappers/operation.mapper';
|
|
import { TBANK_CACHE_KEYS } from '../tbank.config';
|
|
import { EntityNotFoundException } from '../../../common/exceptions/entity-not-found.exception';
|
|
import type { BrokerOperationsPage } from '../types/broker.types';
|
|
import type { TBankOperationsByCursorResponse } from '../types/tbank-proto.types';
|
|
import { BrokerAccountsService } from './broker-accounts.service';
|
|
import { TBankClientService } from './tbank-client.service';
|
|
|
|
@Injectable()
|
|
export class BrokerOperationsService {
|
|
constructor(
|
|
private readonly accountsService: BrokerAccountsService,
|
|
private readonly tbankClient: TBankClientService,
|
|
private readonly cacheService: CacheService,
|
|
) {}
|
|
|
|
async getOperations(
|
|
accountId: string,
|
|
query: BrokerOperationQueryDto,
|
|
): Promise<ApiEnvelopePayload<BrokerOperationsPage>> {
|
|
const account = await this.accountsService.findById(accountId);
|
|
if (!account) throw new EntityNotFoundException('BrokerAccount', accountId);
|
|
|
|
const request = this.buildRequest(accountId, query);
|
|
const result = await this.cacheService.getOrFetch(
|
|
TBANK_CACHE_KEYS.operations,
|
|
[accountId, JSON.stringify(request)],
|
|
() => this.fetchOperations(accountId, request),
|
|
'tbankOperationsTtl',
|
|
);
|
|
|
|
return new ApiEnvelopePayload(result.data, result.fromCache, result.cachedAt);
|
|
}
|
|
|
|
private buildRequest(accountId: string, query: BrokerOperationQueryDto): Record<string, unknown> {
|
|
const now = new Date();
|
|
const startOfYear = new Date(Date.UTC(now.getUTCFullYear(), 0, 1));
|
|
const operationTypes = query.operationTypes
|
|
? query.operationTypes
|
|
.split(',')
|
|
.map((value) => value.trim())
|
|
.filter(Boolean)
|
|
: undefined;
|
|
|
|
return {
|
|
accountId,
|
|
instrumentId: query.instrumentId,
|
|
from: {
|
|
seconds: Math.floor(new Date(query.from ?? startOfYear.toISOString()).getTime() / 1000),
|
|
},
|
|
to: {
|
|
seconds: Math.floor(new Date(query.to ?? now.toISOString()).getTime() / 1000),
|
|
},
|
|
cursor: query.cursor,
|
|
limit: query.limit ?? 100,
|
|
operationTypes,
|
|
state: query.state ?? 'OPERATION_STATE_EXECUTED',
|
|
withoutCommissions: false,
|
|
withoutTrades: false,
|
|
withoutOvernights: false,
|
|
};
|
|
}
|
|
|
|
private async fetchOperations(
|
|
accountId: string,
|
|
request: Record<string, unknown>,
|
|
): Promise<BrokerOperationsPage> {
|
|
const operationsClient = this.tbankClient.getServiceClient('OperationsService') as any;
|
|
const response = await this.tbankClient.callUnary<
|
|
Record<string, unknown>,
|
|
TBankOperationsByCursorResponse
|
|
>(
|
|
'OperationsService/GetOperationsByCursor',
|
|
operationsClient.getOperationsByCursor.bind(operationsClient),
|
|
request,
|
|
);
|
|
|
|
return mapOperationsPage(accountId, response);
|
|
}
|
|
}
|