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
This commit is contained in:
Sergey Krylov 2026-06-25 20:30:45 +03:00
parent 3b919ecdc6
commit 9f85dc5dde
22 changed files with 128 additions and 79 deletions

View File

@ -0,0 +1,8 @@
import { HttpException, HttpStatus } from '@nestjs/common';
export abstract class DomainException extends HttpException {
constructor(message: string, status: HttpStatus) {
super(message, status);
this.name = this.constructor.name;
}
}

View File

@ -0,0 +1,8 @@
import { HttpStatus } from '@nestjs/common';
import { DomainException } from './domain.exception';
export class EntityNotFoundException extends DomainException {
constructor(entity: string, id: string | number) {
super(`${entity} ${id} not found`, HttpStatus.NOT_FOUND);
}
}

View File

@ -0,0 +1,8 @@
import { HttpStatus } from '@nestjs/common';
import { DomainException } from './domain.exception';
export class MoexApiException extends DomainException {
constructor(message: string) {
super(`MOEX API error: ${message}`, HttpStatus.BAD_GATEWAY);
}
}

View File

@ -0,0 +1,8 @@
import { HttpStatus } from '@nestjs/common';
import { DomainException } from './domain.exception';
export class PortfolioAccessDeniedException extends DomainException {
constructor(portfolioId: number) {
super(`Access denied to portfolio ${portfolioId}`, HttpStatus.FORBIDDEN);
}
}

View File

@ -0,0 +1,14 @@
import { HttpStatus } from '@nestjs/common';
import { DomainException } from './domain.exception';
export class TBankApiException extends DomainException {
constructor(message: string) {
super(`T-Bank API error: ${message}`, HttpStatus.BAD_GATEWAY);
}
}
export class TBankNotConfiguredException extends DomainException {
constructor() {
super('T-Bank integration is not configured', HttpStatus.SERVICE_UNAVAILABLE);
}
}

View File

@ -1,8 +1,10 @@
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, Logger } from '@nestjs/common';
import { Response } from 'express';
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(HttpExceptionFilter.name);
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
@ -25,6 +27,7 @@ export class HttpExceptionFilter implements ExceptionFilter {
}
} else if (exception instanceof Error) {
message = exception.message;
this.logger.error(`Unhandled exception: ${exception.message}`, exception.stack);
}
response.status(status).json({

View File

@ -1,5 +1,5 @@
import { NotFoundException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { EntityNotFoundException } from '../../common/exceptions/entity-not-found.exception';
import { BondsService } from './bonds.service';
import { MoexClientService } from '../moex-client/moex-client.service';
import { CacheService } from '../cache/cache.service';
@ -135,10 +135,10 @@ describe('BondsService', () => {
expect(result.data.marketData.updatedAt).toMatch(/T18:45:00$/);
});
it('throws NotFoundException when bond data is missing', async () => {
it('throws EntityNotFoundException when bond data is missing', async () => {
vi.mocked(moexClient.getBondData).mockResolvedValue(null);
await expect(service.getBond('UNKNOWN')).rejects.toBeInstanceOf(NotFoundException);
await expect(service.getBond('UNKNOWN')).rejects.toBeInstanceOf(EntityNotFoundException);
expect(cache.getOrFetch).toHaveBeenCalledTimes(1);
expect(moexClient.getBondMarketData).not.toHaveBeenCalled();
});

View File

@ -1,7 +1,8 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { MoexClientService } from '../moex-client/moex-client.service';
import { CacheService } from '../cache/cache.service';
import { ApiEnvelopePayload } from '../../common/dto/api-response.dto';
import { EntityNotFoundException } from '../../common/exceptions/entity-not-found.exception';
@Injectable()
export class BondsService {
@ -23,7 +24,7 @@ export class BondsService {
);
if (!bond) {
throw new NotFoundException(`Bond ${secid} not found`);
throw new EntityNotFoundException('Bond', secid);
}
const { data: mkt } = await this.cache.getOrFetch(
@ -89,7 +90,7 @@ export class BondsService {
);
if (!mkt) {
throw new NotFoundException(`Market data for bond ${secid} not found`);
throw new EntityNotFoundException('MarketData', `bond ${secid}`);
}
return new ApiEnvelopePayload(

View File

@ -5,7 +5,8 @@ import { PrismaService } from '../prisma/prisma.service';
import { MoexClientService } from '../moex-client/moex-client.service';
import { CacheService } from '../cache/cache.service';
import configuration from '../../config/configuration';
import { ForbiddenException, NotFoundException } from '@nestjs/common';
import { EntityNotFoundException } from '../../common/exceptions/entity-not-found.exception';
import { PortfolioAccessDeniedException } from '../../common/exceptions/portfolio-access.exception';
describe('PortfolioService', () => {
let service: PortfolioService;
@ -204,14 +205,14 @@ describe('PortfolioService', () => {
});
describe('findOne', () => {
it('should throw NotFoundException for non-existent portfolio', async () => {
it('should throw EntityNotFoundException for non-existent portfolio', async () => {
vi.mocked(prisma.portfolio.findUnique).mockResolvedValue(null);
await expect(service.findOne(1, 999)).rejects.toThrow(NotFoundException);
await expect(service.findOne(1, 999)).rejects.toThrow(EntityNotFoundException);
});
it('should throw ForbiddenException for wrong user', async () => {
it('should throw PortfolioAccessDeniedException for wrong user', async () => {
vi.mocked(prisma.portfolio.findUnique).mockResolvedValue(mockPortfolio({ userId: 2 }) as any);
await expect(service.findOne(1, 1)).rejects.toThrow(ForbiddenException);
await expect(service.findOne(1, 1)).rejects.toThrow(PortfolioAccessDeniedException);
});
it('should return portfolio with enriched positions and analytics summary', async () => {
@ -525,16 +526,16 @@ describe('PortfolioService', () => {
expect(result.summary.weightedYield).toBeCloseTo(0, 1);
});
it('should throw ForbiddenException if portfolio belongs to another user', async () => {
it('should throw PortfolioAccessDeniedException if portfolio belongs to another user', async () => {
vi.mocked(prisma.portfolio.findUnique).mockResolvedValue(mockPortfolio({ userId: 2 }) as any);
await expect(service.getAnalytics(1, 1)).rejects.toThrow(ForbiddenException);
await expect(service.getAnalytics(1, 1)).rejects.toThrow(PortfolioAccessDeniedException);
});
it('should throw NotFoundException if portfolio does not exist', async () => {
it('should throw EntityNotFoundException if portfolio does not exist', async () => {
vi.mocked(prisma.portfolio.findUnique).mockResolvedValue(null);
await expect(service.getAnalytics(1, 999)).rejects.toThrow(NotFoundException);
await expect(service.getAnalytics(1, 999)).rejects.toThrow(EntityNotFoundException);
});
});
});

View File

@ -1,12 +1,12 @@
import {
Injectable,
NotFoundException,
BadRequestException,
ForbiddenException,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { MoexClientService } from '../moex-client/moex-client.service';
import { CacheService } from '../cache/cache.service';
import { EntityNotFoundException } from '../../common/exceptions/entity-not-found.exception';
import { PortfolioAccessDeniedException } from '../../common/exceptions/portfolio-access.exception';
import type {
MoexShareMarketData,
MoexBondPositionData,
@ -135,8 +135,8 @@ export class PortfolioService {
include: { positions: true },
});
if (!portfolio) throw new NotFoundException(`Portfolio ${id} not found`);
if (portfolio.userId !== userId) throw new ForbiddenException('Access denied');
if (!portfolio) throw new EntityNotFoundException('Portfolio', id);
if (portfolio.userId !== userId) throw new PortfolioAccessDeniedException(id);
const positionsWithPrices = await this.enrichPositions(portfolio.positions, id);
@ -168,8 +168,8 @@ export class PortfolioService {
async update(userId: number, id: number, dto: UpdatePortfolioDto) {
const portfolio = await this.prisma.portfolio.findUnique({ where: { id } });
if (!portfolio) throw new NotFoundException(`Portfolio ${id} not found`);
if (portfolio.userId !== userId) throw new ForbiddenException('Access denied');
if (!portfolio) throw new EntityNotFoundException('Portfolio', id);
if (portfolio.userId !== userId) throw new PortfolioAccessDeniedException(id);
const updated = await this.prisma.portfolio.update({
where: { id },
@ -189,8 +189,8 @@ export class PortfolioService {
async remove(userId: number, id: number) {
const portfolio = await this.prisma.portfolio.findUnique({ where: { id } });
if (!portfolio) throw new NotFoundException(`Portfolio ${id} not found`);
if (portfolio.userId !== userId) throw new ForbiddenException('Access denied');
if (!portfolio) throw new EntityNotFoundException('Portfolio', id);
if (portfolio.userId !== userId) throw new PortfolioAccessDeniedException(id);
await this.prisma.portfolio.delete({ where: { id } });
}
@ -200,8 +200,8 @@ export class PortfolioService {
where: { id: portfolioId },
include: { positions: true },
});
if (!portfolio) throw new NotFoundException(`Portfolio ${portfolioId} not found`);
if (portfolio.userId !== userId) throw new ForbiddenException('Access denied');
if (!portfolio) throw new EntityNotFoundException('Portfolio', portfolioId);
if (portfolio.userId !== userId) throw new PortfolioAccessDeniedException(portfolioId);
const exists = portfolio.positions.find((p) => p.secid === dto.secid);
if (exists)
@ -235,12 +235,12 @@ export class PortfolioService {
dto: UpdatePositionDto,
) {
const portfolio = await this.prisma.portfolio.findUnique({ where: { id: portfolioId } });
if (!portfolio) throw new NotFoundException(`Portfolio ${portfolioId} not found`);
if (portfolio.userId !== userId) throw new ForbiddenException('Access denied');
if (!portfolio) throw new EntityNotFoundException('Portfolio', portfolioId);
if (portfolio.userId !== userId) throw new PortfolioAccessDeniedException(portfolioId);
const position = await this.prisma.position.findUnique({ where: { id: positionId } });
if (!position || position.portfolioId !== portfolioId) {
throw new NotFoundException(`Position ${positionId} not found`);
throw new EntityNotFoundException('Position', positionId);
}
return this.prisma.position.update({
@ -257,12 +257,12 @@ export class PortfolioService {
async removePosition(userId: number, portfolioId: number, positionId: number) {
const portfolio = await this.prisma.portfolio.findUnique({ where: { id: portfolioId } });
if (!portfolio) throw new NotFoundException(`Portfolio ${portfolioId} not found`);
if (portfolio.userId !== userId) throw new ForbiddenException('Access denied');
if (!portfolio) throw new EntityNotFoundException('Portfolio', portfolioId);
if (portfolio.userId !== userId) throw new PortfolioAccessDeniedException(portfolioId);
const position = await this.prisma.position.findUnique({ where: { id: positionId } });
if (!position || position.portfolioId !== portfolioId) {
throw new NotFoundException(`Position ${positionId} not found`);
throw new EntityNotFoundException('Position', positionId);
}
await this.prisma.position.delete({ where: { id: positionId } });
@ -517,8 +517,8 @@ export class PortfolioService {
async getAnalytics(userId: number, portfolioId: number): Promise<AnalyticsResponseDto> {
const portfolio = await this.prisma.portfolio.findUnique({ where: { id: portfolioId } });
if (!portfolio) throw new NotFoundException(`Portfolio ${portfolioId} not found`);
if (portfolio.userId !== userId) throw new ForbiddenException();
if (!portfolio) throw new EntityNotFoundException('Portfolio', portfolioId);
if (portfolio.userId !== userId) throw new PortfolioAccessDeniedException(portfolioId);
const enrichedPositions = await this.getPositionsWithPrices(portfolioId);

View File

@ -1,5 +1,5 @@
import { NotFoundException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { EntityNotFoundException } from '../../common/exceptions/entity-not-found.exception';
import { SharesService } from './shares.service';
import { MoexClientService } from '../moex-client/moex-client.service';
import { CacheService } from '../cache/cache.service';
@ -109,7 +109,7 @@ describe('SharesService', () => {
expect(result.data.marketData.updatedAt).toMatch(/T18:45:00$/);
});
it('throws NotFoundException for non-share security', async () => {
it('throws EntityNotFoundException for non-share security', async () => {
vi.mocked(moexClient.getSecurityDescription).mockResolvedValue({
secid: 'SU26238RMFS5',
isin: 'RU000A1038V6',
@ -129,7 +129,7 @@ describe('SharesService', () => {
eveningSession: false,
});
await expect(service.getShare('SU26238RMFS5')).rejects.toBeInstanceOf(NotFoundException);
await expect(service.getShare('SU26238RMFS5')).rejects.toBeInstanceOf(EntityNotFoundException);
expect(cache.getOrFetch).not.toHaveBeenCalled();
});
});

View File

@ -1,7 +1,8 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { MoexClientService } from '../moex-client/moex-client.service';
import { CacheService } from '../cache/cache.service';
import { ApiEnvelopePayload } from '../../common/dto/api-response.dto';
import { EntityNotFoundException } from '../../common/exceptions/entity-not-found.exception';
@Injectable()
export class SharesService {
@ -20,7 +21,7 @@ export class SharesService {
desc.type === 'preferred_share'
)
) {
throw new NotFoundException(`Share ${secid} not found`);
throw new EntityNotFoundException('Share', secid);
}
const {
@ -83,7 +84,7 @@ export class SharesService {
);
if (!marketData) {
throw new NotFoundException(`Market data for ${secid} not found`);
throw new EntityNotFoundException('MarketData', secid);
}
return new ApiEnvelopePayload(

View File

@ -1,5 +1,5 @@
import { NotFoundException } from '@nestjs/common';
import { CacheService } from '../../cache/cache.service';
import { EntityNotFoundException } from '../../../common/exceptions/entity-not-found.exception';
import { BrokerAccountsService } from './broker-accounts.service';
import { BrokerAnalyticsService } from './broker-analytics.service';
import { PrismaService } from '../../prisma/prisma.service';
@ -40,7 +40,7 @@ describe('BrokerAnalyticsService', () => {
vi.mocked(accounts.findById).mockResolvedValue(null);
const service = new BrokerAnalyticsService(prisma, accounts, cache);
await expect(service.getAnalytics('missing')).rejects.toThrow(NotFoundException);
await expect(service.getAnalytics('missing')).rejects.toThrow(EntityNotFoundException);
});
it('returns zeros for account with no operations', async () => {

View File

@ -1,10 +1,11 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service';
import { CacheService } from '../../cache/cache.service';
import { ApiEnvelopePayload } from '../../../common/dto/api-response.dto';
import { BrokerAnalyticsDto } from '../dto/broker-analytics-response.dto';
import { BrokerAccountsService } from './broker-accounts.service';
import { TBANK_CACHE_KEYS } from '../tbank.config';
import { BrokerAnalyticsDto } from '../dto/broker-analytics-response.dto';
import { EntityNotFoundException } from '../../../common/exceptions/entity-not-found.exception';
import { ApiEnvelopePayload } from '../../../common/dto/api-response.dto';
import { BrokerAccountsService } from './broker-accounts.service';
const DEPOSIT_TYPES = new Set([
'OPERATION_TYPE_INPUT',
@ -44,7 +45,7 @@ export class BrokerAnalyticsService {
async getAnalytics(accountId: string): Promise<ApiEnvelopePayload<BrokerAnalyticsDto>> {
const account = await this.accountsService.findById(accountId);
if (!account) throw new NotFoundException('Broker account not found');
if (!account) throw new EntityNotFoundException('BrokerAccount', accountId);
const result = await this.cacheService.getOrFetch(
TBANK_CACHE_KEYS.analytics,

View File

@ -1,5 +1,5 @@
import { NotFoundException } from '@nestjs/common';
import { CacheService } from '../../cache/cache.service';
import { EntityNotFoundException } from '../../../common/exceptions/entity-not-found.exception';
import { MoexClientService } from '../../moex-client/moex-client.service';
import { BrokerAccountsService } from './broker-accounts.service';
import { BrokerEventsService } from './broker-events.service';
@ -45,7 +45,7 @@ describe('BrokerEventsService', () => {
const service = new BrokerEventsService(accounts, portfolio, moex, operations, cache);
await expect(
service.getEvents('missing', { from: '2026-06-01', to: '2026-07-01' }),
).rejects.toThrow(NotFoundException);
).rejects.toThrow(EntityNotFoundException);
});
it('returns empty events for account with no positions', async () => {

View File

@ -1,7 +1,8 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { CacheService } from '../../cache/cache.service';
import { MoexClientService } from '../../moex-client/moex-client.service';
import { ApiEnvelopePayload } from '../../../common/dto/api-response.dto';
import { EntityNotFoundException } from '../../../common/exceptions/entity-not-found.exception';
import { TBANK_CACHE_KEYS } from '../tbank.config';
import { mapQuotationToNumber } from '../mappers/money.mapper';
import type {
@ -55,7 +56,7 @@ export class BrokerEventsService {
query: BrokerEventsQuery,
): Promise<ApiEnvelopePayload<BrokerEventsData>> {
const account = await this.accountsService.findById(accountId);
if (!account) throw new NotFoundException('Broker account not found');
if (!account) throw new EntityNotFoundException('BrokerAccount', accountId);
const eventTypes = this.parseEventTypes(query.types);
const eventTypeKey = Array.from(eventTypes).join(',');

View File

@ -1,5 +1,5 @@
import { NotFoundException } from '@nestjs/common';
import { CacheService } from '../../cache/cache.service';
import { EntityNotFoundException } from '../../../common/exceptions/entity-not-found.exception';
import { BrokerAccountsService } from './broker-accounts.service';
import { BrokerOperationsService } from './broker-operations.service';
import { TBankClientService } from './tbank-client.service';
@ -17,7 +17,7 @@ describe('BrokerOperationsService', () => {
vi.mocked(accounts.findById).mockResolvedValue(null);
const service = new BrokerOperationsService(accounts, client, cache);
await expect(service.getOperations('missing', {})).rejects.toThrow(NotFoundException);
await expect(service.getOperations('missing', {})).rejects.toThrow(EntityNotFoundException);
});
it('builds cursor request and maps operation page', async () => {

View File

@ -1,9 +1,10 @@
import { Injectable, NotFoundException } from '@nestjs/common';
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';
@ -22,7 +23,7 @@ export class BrokerOperationsService {
query: BrokerOperationQueryDto,
): Promise<ApiEnvelopePayload<BrokerOperationsPage>> {
const account = await this.accountsService.findById(accountId);
if (!account) throw new NotFoundException('Broker account not found');
if (!account) throw new EntityNotFoundException('BrokerAccount', accountId);
const request = this.buildRequest(accountId, query);
const result = await this.cacheService.getOrFetch(

View File

@ -1,5 +1,5 @@
import { NotFoundException } from '@nestjs/common';
import { CacheService } from '../../cache/cache.service';
import { EntityNotFoundException } from '../../../common/exceptions/entity-not-found.exception';
import { BrokerAccountsService } from './broker-accounts.service';
import { BrokerInstrumentsService } from './broker-instruments.service';
import { BrokerPortfolioService } from './broker-portfolio.service';
@ -19,7 +19,7 @@ describe('BrokerPortfolioService', () => {
vi.mocked(accounts.findById).mockResolvedValue(null);
const service = new BrokerPortfolioService(accounts, instruments, client, cache);
await expect(service.getPortfolio('missing')).rejects.toThrow(NotFoundException);
await expect(service.getPortfolio('missing')).rejects.toThrow(EntityNotFoundException);
});
it('fetches portfolio through cache without positions', async () => {
@ -98,7 +98,7 @@ describe('BrokerPortfolioService', () => {
vi.mocked(accounts.findById).mockResolvedValue(null);
const service = new BrokerPortfolioService(accounts, instruments, client, cache);
await expect(service.getPositions('missing')).rejects.toThrow(NotFoundException);
await expect(service.getPositions('missing')).rejects.toThrow(EntityNotFoundException);
});
it('returns first page of positions', async () => {

View File

@ -1,7 +1,8 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { CacheService } from '../../cache/cache.service';
import { mapBrokerPortfolio, mapBrokerPositionsPage } from '../mappers/portfolio.mapper';
import { TBANK_CACHE_KEYS } from '../tbank.config';
import { EntityNotFoundException } from '../../../common/exceptions/entity-not-found.exception';
import type { BrokerPortfolio, BrokerPositionsPage } from '../types/broker.types';
import type {
TBankInstrument,
@ -25,7 +26,7 @@ export class BrokerPortfolioService {
async getPortfolio(accountId: string): Promise<ApiEnvelopePayload<BrokerPortfolio>> {
const account = await this.accountsService.findById(accountId);
if (!account) throw new NotFoundException('Broker account not found');
if (!account) throw new EntityNotFoundException('BrokerAccount', accountId);
const result = await this.cacheService.getOrFetch(
TBANK_CACHE_KEYS.portfolio,
@ -51,7 +52,7 @@ export class BrokerPortfolioService {
type?: string,
): Promise<ApiEnvelopePayload<BrokerPositionsPage>> {
const account = await this.accountsService.findById(accountId);
if (!account) throw new NotFoundException('Broker account not found');
if (!account) throw new EntityNotFoundException('BrokerAccount', accountId);
const result = await this.cacheService.getOrFetch(
TBANK_CACHE_KEYS.positions,

View File

@ -1,5 +1,5 @@
import { ServiceUnavailableException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { TBankNotConfiguredException } from '../../../common/exceptions/tbank-api.exception';
import { ChannelCredentials, ClientUnaryCall, Metadata, ServiceError, status } from '@grpc/grpc-js';
import { mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
@ -85,7 +85,7 @@ describe('TBankClientService', () => {
},
{},
),
).rejects.toThrow(ServiceUnavailableException);
).rejects.toThrow(TBankNotConfiguredException);
});
it('wraps grpc errors with status code and tracking id', async () => {
@ -107,9 +107,7 @@ describe('TBankClientService', () => {
{},
),
).rejects.toMatchObject({
response: expect.objectContaining({
message: expect.stringContaining('T-Bank upstream error'),
}),
response: expect.stringContaining('T-Bank upstream error'),
});
});

View File

@ -1,10 +1,6 @@
import {
BadGatewayException,
Injectable,
Logger,
ServiceUnavailableException,
} from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { TBankNotConfiguredException, TBankApiException } from '../../../common/exceptions/tbank-api.exception';
import {
CallOptions,
ChannelCredentials,
@ -79,7 +75,7 @@ export class TBankClientService {
createMetadata(): Metadata {
const token = this.configService.get<string>('app.tbank.token', '');
if (!token) {
throw new ServiceUnavailableException('T-Bank integration is not configured');
throw new TBankNotConfiguredException();
}
const metadata = new Metadata();
@ -169,7 +165,7 @@ export class TBankClientService {
error instanceof Error ? error.message : String(error)
}`,
);
throw new ServiceUnavailableException('T-Bank CA certificate is not readable');
throw new TBankApiException('T-Bank CA certificate is not readable');
}
}
@ -191,10 +187,9 @@ export class TBankClientService {
}),
);
return new BadGatewayException({
message: publicMessage,
trackingId: trackingId ? String(trackingId) : null,
retryAfter: retryAfter ? String(retryAfter) : null,
});
const detail = [publicMessage, trackingId && `trackingId:${trackingId}`, retryAfter && `retryAfter:${retryAfter}`]
.filter(Boolean)
.join('; ');
return new TBankApiException(detail);
}
}