moex-vibe/apps/backend/src/modules/tbank/services/broker-operations.service.ts
Sergey Krylov 9f85dc5dde refactor(backend): introduce domain exception hierarchy
- 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
2026-06-25 20:30:45 +03:00

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);
}
}