fix(api): add error handling to remaining 51 CQRS handlers across 8 modules

Wraps every handler's execute() method in a try-catch block that:
- Re-throws DomainExceptions to preserve structured error responses
- Logs unexpected infrastructure errors with full context
- Throws InternalServerErrorException with Vietnamese user message

Modules updated:
- auth (11 handlers: register, refresh-token, verify-kyc, deletions, profile queries)
- listings (7 handlers: create, moderate, upload, status, search, queries)
- payments (5 handlers: create, callback, refund, status, transactions)
- subscriptions (7 handlers: create, cancel, upgrade, meter, quota, billing, plans)
- analytics (8 handlers: reports, events, market-index, district, heatmap, trends, valuation)
- search (9 handlers: saved-search CRUD, reindex, sync, geo-search, properties)
- notifications (1 handler: send-notification)
- agents (3 handlers: quality-score, dashboard, public-profile)

Combined with the previous commit (29 handlers in admin, inquiries, leads, reviews),
all 80+ CQRS handlers now have comprehensive error handling.

Verification:
- pnpm typecheck: 0 errors
- pnpm test: 1387 tests passed (228 files)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-11 20:04:42 +07:00
parent 7008230424
commit 18e50a9649
51 changed files with 1998 additions and 1499 deletions

View File

@@ -1,7 +1,7 @@
import { Inject } from '@nestjs/common';
import { Inject, InternalServerErrorException } from '@nestjs/common';
import { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
import { createId } from '@paralleldrive/cuid2';
import { ValidationException, type CacheService, CachePrefix, type LoggerService } from '@modules/shared';
import { DomainException, ValidationException, type CacheService, CachePrefix, type LoggerService } from '@modules/shared';
import { ListingEntity } from '../../../domain/entities/listing.entity';
import { PropertyEntity } from '../../../domain/entities/property.entity';
import { LISTING_REPOSITORY, type IListingRepository } from '../../../domain/repositories/listing.repository';
@@ -51,126 +51,136 @@ export class CreateListingHandler implements ICommandHandler<CreateListingComman
) {}
async execute(command: CreateListingCommand): Promise<CreateListingResult> {
// Validate value objects
const addressResult = Address.create(command.address, command.ward, command.district, command.city);
if (addressResult.isErr) throw new ValidationException(addressResult.unwrapErr());
const geoPointResult = GeoPoint.create(command.latitude, command.longitude);
if (geoPointResult.isErr) throw new ValidationException(geoPointResult.unwrapErr());
const priceResult = Price.create(command.priceVND);
if (priceResult.isErr) throw new ValidationException(priceResult.unwrapErr());
const address = addressResult.unwrap();
const geoPoint = geoPointResult.unwrap();
const price = priceResult.unwrap();
// Create property
const propertyId = createId();
const property = PropertyEntity.createNew(propertyId, {
propertyType: command.propertyType,
title: command.title,
description: command.description,
address,
location: geoPoint,
areaM2: command.areaM2,
usableAreaM2: command.usableAreaM2 ?? null,
bedrooms: command.bedrooms ?? null,
bathrooms: command.bathrooms ?? null,
floors: command.floors ?? null,
floor: command.floor ?? null,
totalFloors: command.totalFloors ?? null,
direction: command.direction ?? null,
yearBuilt: command.yearBuilt ?? null,
legalStatus: command.legalStatus ?? null,
amenities: command.amenities ?? null,
nearbyPOIs: command.nearbyPOIs ?? null,
metroDistanceM: command.metroDistanceM ?? null,
projectName: command.projectName ?? null,
});
await this.propertyRepo.save(property);
// Create listing
const listingId = createId();
const listing = ListingEntity.createNew(
listingId,
propertyId,
command.sellerId,
command.transactionType,
price,
command.areaM2,
command.agentId,
command.rentPriceMonthly,
command.commissionPct,
);
await this.listingRepo.save(listing);
// Publish domain events
const events = [...property.clearDomainEvents(), ...listing.clearDomainEvents()];
for (const event of events) {
this.eventBus.publish(event);
}
await Promise.all([
this.cache.invalidateByPrefix(CachePrefix.SEARCH),
this.cache.invalidateByPrefix(CachePrefix.MARKET_DISTRICT),
this.cache.invalidateByPrefix(CachePrefix.MARKET_REPORT),
]);
// Duplicate detection — flag but never block creation
let duplicateWarnings: DuplicateWarning[] = [];
try {
const candidates = await this.duplicateDetector.findDuplicates({
excludePropertyId: propertyId,
latitude: command.latitude,
longitude: command.longitude,
// Validate value objects
const addressResult = Address.create(command.address, command.ward, command.district, command.city);
if (addressResult.isErr) throw new ValidationException(addressResult.unwrapErr());
const geoPointResult = GeoPoint.create(command.latitude, command.longitude);
if (geoPointResult.isErr) throw new ValidationException(geoPointResult.unwrapErr());
const priceResult = Price.create(command.priceVND);
if (priceResult.isErr) throw new ValidationException(priceResult.unwrapErr());
const address = addressResult.unwrap();
const geoPoint = geoPointResult.unwrap();
const price = priceResult.unwrap();
// Create property
const propertyId = createId();
const property = PropertyEntity.createNew(propertyId, {
propertyType: command.propertyType,
title: command.title,
propertyType: command.propertyType,
});
duplicateWarnings = candidates.map((c) => ({
listingId: c.listingId,
propertyId: c.propertyId,
title: c.title,
address: c.address,
district: c.district,
distanceMeters: c.distanceMeters,
titleSimilarity: c.titleSimilarity,
}));
} catch {
this.logger.warn('Duplicate detection failed — listing created without warnings', 'CreateListingHandler');
}
// Price validation — flag but never block creation
let priceWarning: PriceWarning | undefined;
try {
const priceResult = await this.priceValidator.validate({
priceVND: command.priceVND,
description: command.description,
address,
location: geoPoint,
areaM2: command.areaM2,
propertyType: command.propertyType,
district: command.district,
usableAreaM2: command.usableAreaM2 ?? null,
bedrooms: command.bedrooms ?? null,
bathrooms: command.bathrooms ?? null,
floors: command.floors ?? null,
floor: command.floor ?? null,
totalFloors: command.totalFloors ?? null,
direction: command.direction ?? null,
yearBuilt: command.yearBuilt ?? null,
legalStatus: command.legalStatus ?? null,
amenities: command.amenities ?? null,
nearbyPOIs: command.nearbyPOIs ?? null,
metroDistanceM: command.metroDistanceM ?? null,
projectName: command.projectName ?? null,
});
if (priceResult.isSuspicious) {
priceWarning = {
pricePerM2: priceResult.pricePerM2,
minPricePerM2: priceResult.minPricePerM2,
maxPricePerM2: priceResult.maxPricePerM2,
reason: priceResult.reason!,
};
}
} catch {
this.logger.warn('Price validation failed — listing created without price warning', 'CreateListingHandler');
}
await this.propertyRepo.save(property);
return {
listingId,
propertyId,
status: listing.status,
duplicateWarnings,
priceWarning,
};
// Create listing
const listingId = createId();
const listing = ListingEntity.createNew(
listingId,
propertyId,
command.sellerId,
command.transactionType,
price,
command.areaM2,
command.agentId,
command.rentPriceMonthly,
command.commissionPct,
);
await this.listingRepo.save(listing);
// Publish domain events
const events = [...property.clearDomainEvents(), ...listing.clearDomainEvents()];
for (const event of events) {
this.eventBus.publish(event);
}
await Promise.all([
this.cache.invalidateByPrefix(CachePrefix.SEARCH),
this.cache.invalidateByPrefix(CachePrefix.MARKET_DISTRICT),
this.cache.invalidateByPrefix(CachePrefix.MARKET_REPORT),
]);
// Duplicate detection — flag but never block creation
let duplicateWarnings: DuplicateWarning[] = [];
try {
const candidates = await this.duplicateDetector.findDuplicates({
excludePropertyId: propertyId,
latitude: command.latitude,
longitude: command.longitude,
title: command.title,
propertyType: command.propertyType,
});
duplicateWarnings = candidates.map((c) => ({
listingId: c.listingId,
propertyId: c.propertyId,
title: c.title,
address: c.address,
district: c.district,
distanceMeters: c.distanceMeters,
titleSimilarity: c.titleSimilarity,
}));
} catch {
this.logger.warn('Duplicate detection failed — listing created without warnings', 'CreateListingHandler');
}
// Price validation — flag but never block creation
let priceWarning: PriceWarning | undefined;
try {
const priceResult = await this.priceValidator.validate({
priceVND: command.priceVND,
areaM2: command.areaM2,
propertyType: command.propertyType,
district: command.district,
});
if (priceResult.isSuspicious) {
priceWarning = {
pricePerM2: priceResult.pricePerM2,
minPricePerM2: priceResult.minPricePerM2,
maxPricePerM2: priceResult.maxPricePerM2,
reason: priceResult.reason!,
};
}
} catch {
this.logger.warn('Price validation failed — listing created without price warning', 'CreateListingHandler');
}
return {
listingId,
propertyId,
status: listing.status,
duplicateWarnings,
priceWarning,
};
} catch (error) {
if (error instanceof DomainException) throw error;
this.logger.error(
`Failed to create listing: ${error instanceof Error ? error.message : error}`,
error instanceof Error ? error.stack : undefined,
this.constructor.name,
);
throw new InternalServerErrorException('Không thể tạo tin đăng bất động sản');
}
}
}

View File

@@ -1,6 +1,6 @@
import { Inject } from '@nestjs/common';
import { Inject, InternalServerErrorException } from '@nestjs/common';
import { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
import { NotFoundException, CacheService, CachePrefix } from '@modules/shared';
import { DomainException, NotFoundException, CacheService, CachePrefix, type LoggerService } from '@modules/shared';
import { LISTING_REPOSITORY, type IListingRepository } from '../../../domain/repositories/listing.repository';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- NestJS DI needs runtime reference
import { ModerationService } from '../../../domain/services/moderation.service';
@@ -13,32 +13,43 @@ export class ModerateListingHandler implements ICommandHandler<ModerateListingCo
private readonly eventBus: EventBus,
private readonly cache: CacheService,
private readonly moderationService: ModerationService,
private readonly logger: LoggerService,
) {}
async execute(command: ModerateListingCommand): Promise<{ status: string }> {
const listing = await this.listingRepo.findById(command.listingId);
if (!listing) {
throw new NotFoundException('Listing', command.listingId);
try {
const listing = await this.listingRepo.findById(command.listingId);
if (!listing) {
throw new NotFoundException('Listing', command.listingId);
}
this.moderationService.applyModeration(listing, {
action: command.action,
moderationScore: command.moderationScore,
notes: command.notes,
});
await this.listingRepo.update(listing);
const events = listing.clearDomainEvents();
for (const event of events) {
this.eventBus.publish(event);
}
await Promise.all([
this.cache.invalidate(CacheService.buildKey(CachePrefix.LISTING, command.listingId)),
this.cache.invalidateByPrefix(CachePrefix.SEARCH),
]);
return { status: listing.status };
} catch (error) {
if (error instanceof DomainException) throw error;
this.logger.error(
`Failed to moderate listing ${command.listingId}: ${error instanceof Error ? error.message : error}`,
error instanceof Error ? error.stack : undefined,
this.constructor.name,
);
throw new InternalServerErrorException('Không thể kiểm duyệt tin đăng');
}
this.moderationService.applyModeration(listing, {
action: command.action,
moderationScore: command.moderationScore,
notes: command.notes,
});
await this.listingRepo.update(listing);
const events = listing.clearDomainEvents();
for (const event of events) {
this.eventBus.publish(event);
}
await Promise.all([
this.cache.invalidate(CacheService.buildKey(CachePrefix.LISTING, command.listingId)),
this.cache.invalidateByPrefix(CachePrefix.SEARCH),
]);
return { status: listing.status };
}
}

View File

@@ -1,6 +1,6 @@
import { Inject } from '@nestjs/common';
import { Inject, InternalServerErrorException } from '@nestjs/common';
import { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
import { NotFoundException, CacheService, CachePrefix } from '@modules/shared';
import { DomainException, NotFoundException, CacheService, CachePrefix, type LoggerService } from '@modules/shared';
import { LISTING_REPOSITORY, type IListingRepository } from '../../../domain/repositories/listing.repository';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- NestJS DI needs runtime reference
import { ModerationService } from '../../../domain/services/moderation.service';
@@ -13,32 +13,43 @@ export class UpdateListingStatusHandler implements ICommandHandler<UpdateListing
private readonly eventBus: EventBus,
private readonly cache: CacheService,
private readonly moderationService: ModerationService,
private readonly logger: LoggerService,
) {}
async execute(command: UpdateListingStatusCommand): Promise<{ status: string }> {
const listing = await this.listingRepo.findById(command.listingId);
if (!listing) {
throw new NotFoundException('Listing', command.listingId);
try {
const listing = await this.listingRepo.findById(command.listingId);
if (!listing) {
throw new NotFoundException('Listing', command.listingId);
}
this.moderationService.applyStatusTransition(
listing,
command.newStatus,
command.moderationNotes,
);
await this.listingRepo.update(listing);
const events = listing.clearDomainEvents();
for (const event of events) {
this.eventBus.publish(event);
}
await Promise.all([
this.cache.invalidate(CacheService.buildKey(CachePrefix.LISTING, command.listingId)),
this.cache.invalidateByPrefix(CachePrefix.SEARCH),
]);
return { status: listing.status };
} catch (error) {
if (error instanceof DomainException) throw error;
this.logger.error(
`Failed to update listing status ${command.listingId}: ${error instanceof Error ? error.message : error}`,
error instanceof Error ? error.stack : undefined,
this.constructor.name,
);
throw new InternalServerErrorException('Không thể cập nhật trạng thái tin đăng');
}
this.moderationService.applyStatusTransition(
listing,
command.newStatus,
command.moderationNotes,
);
await this.listingRepo.update(listing);
const events = listing.clearDomainEvents();
for (const event of events) {
this.eventBus.publish(event);
}
await Promise.all([
this.cache.invalidate(CacheService.buildKey(CachePrefix.LISTING, command.listingId)),
this.cache.invalidateByPrefix(CachePrefix.SEARCH),
]);
return { status: listing.status };
}
}

View File

@@ -1,7 +1,7 @@
import { Inject } from '@nestjs/common';
import { Inject, InternalServerErrorException } from '@nestjs/common';
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
import { createId } from '@paralleldrive/cuid2';
import { type LoggerService, NotFoundException, ValidationException } from '@modules/shared';
import { DomainException, type LoggerService, NotFoundException, ValidationException } from '@modules/shared';
import { PropertyMediaEntity } from '../../../domain/entities/property-media.entity';
import { PROPERTY_REPOSITORY, type IPropertyRepository } from '../../../domain/repositories/property.repository';
import { MEDIA_STORAGE_SERVICE, type IMediaStorageService } from '../../../infrastructure/services/media-storage.service';
@@ -18,47 +18,57 @@ export class UploadMediaHandler implements ICommandHandler<UploadMediaCommand> {
) {}
async execute(command: UploadMediaCommand): Promise<{ mediaId: string; url: string }> {
const property = await this.propertyRepo.findById(command.propertyId);
if (!property) {
throw new NotFoundException('Property', command.propertyId);
}
const mediaCount = await this.propertyRepo.countMediaByPropertyId(command.propertyId);
if (mediaCount >= MAX_MEDIA_PER_PROPERTY) {
throw new ValidationException(`Tối đa ${MAX_MEDIA_PER_PROPERTY} ảnh/video cho mỗi bất động sản`);
}
const mediaType = command.file.mimetype.startsWith('video/') ? 'video' as const : 'image' as const;
let url: string;
try {
url = await this.mediaStorage.upload(
command.file.buffer,
command.file.originalname,
command.file.mimetype,
`properties/${command.propertyId}`,
const property = await this.propertyRepo.findById(command.propertyId);
if (!property) {
throw new NotFoundException('Property', command.propertyId);
}
const mediaCount = await this.propertyRepo.countMediaByPropertyId(command.propertyId);
if (mediaCount >= MAX_MEDIA_PER_PROPERTY) {
throw new ValidationException(`Tối đa ${MAX_MEDIA_PER_PROPERTY} ảnh/video cho mỗi bất động sản`);
}
const mediaType = command.file.mimetype.startsWith('video/') ? 'video' as const : 'image' as const;
let url: string;
try {
url = await this.mediaStorage.upload(
command.file.buffer,
command.file.originalname,
command.file.mimetype,
`properties/${command.propertyId}`,
);
} catch (error) {
this.logger.error(
`Media upload failed for property ${command.propertyId}: ${error instanceof Error ? error.message : error}`,
error instanceof Error ? error.stack : undefined,
'UploadMediaHandler',
);
throw new ValidationException('Tải lên media thất bại, vui lòng thử lại');
}
const mediaId = createId();
const media = PropertyMediaEntity.createNew(
mediaId,
command.propertyId,
url,
mediaType,
mediaCount, // next order index
command.caption,
);
await this.propertyRepo.addMedia(media);
return { mediaId, url };
} catch (error) {
if (error instanceof DomainException) throw error;
this.logger.error(
`Media upload failed for property ${command.propertyId}: ${error instanceof Error ? error.message : error}`,
`Failed to upload media for property ${command.propertyId}: ${error instanceof Error ? error.message : error}`,
error instanceof Error ? error.stack : undefined,
'UploadMediaHandler',
this.constructor.name,
);
throw new ValidationException('Tải lên media thất bại, vui lòng thử lại');
throw new InternalServerErrorException('Không thể tải lên hình ảnh/video cho bất động sản');
}
const mediaId = createId();
const media = PropertyMediaEntity.createNew(
mediaId,
command.propertyId,
url,
mediaType,
mediaCount, // next order index
command.caption,
);
await this.propertyRepo.addMedia(media);
return { mediaId, url };
}
}

View File

@@ -1,6 +1,6 @@
import { Inject } from '@nestjs/common';
import { Inject, InternalServerErrorException } from '@nestjs/common';
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
import { NotFoundException, CacheService, CachePrefix, CacheTTL } from '@modules/shared';
import { DomainException, NotFoundException, CacheService, CachePrefix, CacheTTL, type LoggerService } from '@modules/shared';
import { type ListingDetailData } from '../../../domain/repositories/listing-read.dto';
import { LISTING_REPOSITORY, type IListingRepository } from '../../../domain/repositories/listing.repository';
import { GetListingQuery } from './get-listing.query';
@@ -13,22 +13,33 @@ export class GetListingHandler implements IQueryHandler<GetListingQuery> {
constructor(
@Inject(LISTING_REPOSITORY) private readonly listingRepo: IListingRepository,
private readonly cache: CacheService,
private readonly logger: LoggerService,
) {}
async execute(query: GetListingQuery): Promise<ListingDetailData> {
const cacheKey = CacheService.buildKey(CachePrefix.LISTING, query.listingId);
try {
const cacheKey = CacheService.buildKey(CachePrefix.LISTING, query.listingId);
return this.cache.getOrSet(
cacheKey,
async () => {
const result = await this.listingRepo.findByIdWithProperty(query.listingId);
if (!result) {
throw new NotFoundException('Listing', query.listingId);
}
return result;
},
CacheTTL.LISTING_DETAIL,
'listing',
);
return this.cache.getOrSet(
cacheKey,
async () => {
const result = await this.listingRepo.findByIdWithProperty(query.listingId);
if (!result) {
throw new NotFoundException('Listing', query.listingId);
}
return result;
},
CacheTTL.LISTING_DETAIL,
'listing',
);
} catch (error) {
if (error instanceof DomainException) throw error;
this.logger.error(
`Failed to get listing ${query.listingId}: ${error instanceof Error ? error.message : error}`,
error instanceof Error ? error.stack : undefined,
this.constructor.name,
);
throw new InternalServerErrorException('Không thể lấy thông tin tin đăng');
}
}
}

View File

@@ -1,5 +1,6 @@
import { Inject } from '@nestjs/common';
import { Inject, InternalServerErrorException } from '@nestjs/common';
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
import { DomainException, type LoggerService } from '@modules/shared';
import { type ListingSearchItem } from '../../../domain/repositories/listing-read.dto';
import { LISTING_REPOSITORY, type IListingRepository, type PaginatedResult } from '../../../domain/repositories/listing.repository';
import { GetPendingModerationQuery } from './get-pending-moderation.query';
@@ -8,9 +9,20 @@ import { GetPendingModerationQuery } from './get-pending-moderation.query';
export class GetPendingModerationHandler implements IQueryHandler<GetPendingModerationQuery> {
constructor(
@Inject(LISTING_REPOSITORY) private readonly listingRepo: IListingRepository,
private readonly logger: LoggerService,
) {}
async execute(query: GetPendingModerationQuery): Promise<PaginatedResult<ListingSearchItem>> {
return this.listingRepo.findByStatus('PENDING_REVIEW', query.page, query.limit);
try {
return this.listingRepo.findByStatus('PENDING_REVIEW', query.page, query.limit);
} catch (error) {
if (error instanceof DomainException) throw error;
this.logger.error(
`Failed to get pending moderation listings: ${error instanceof Error ? error.message : error}`,
error instanceof Error ? error.stack : undefined,
this.constructor.name,
);
throw new InternalServerErrorException('Không thể lấy danh sách tin đăng chờ kiểm duyệt');
}
}
}

View File

@@ -1,6 +1,6 @@
import { Inject } from '@nestjs/common';
import { Inject, InternalServerErrorException } from '@nestjs/common';
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
import { CacheService, CachePrefix, CacheTTL } from '@modules/shared';
import { DomainException, CacheService, CachePrefix, CacheTTL, type LoggerService } from '@modules/shared';
import { type ListingSearchItem } from '../../../domain/repositories/listing-read.dto';
import { LISTING_REPOSITORY, type IListingRepository, type PaginatedResult } from '../../../domain/repositories/listing.repository';
import { SearchListingsQuery } from './search-listings.query';
@@ -10,44 +10,55 @@ export class SearchListingsHandler implements IQueryHandler<SearchListingsQuery>
constructor(
@Inject(LISTING_REPOSITORY) private readonly listingRepo: IListingRepository,
private readonly cacheService: CacheService,
private readonly logger: LoggerService,
) {}
async execute(query: SearchListingsQuery): Promise<PaginatedResult<ListingSearchItem>> {
const cacheKey = CacheService.buildKey(
CachePrefix.SEARCH,
query.status,
query.transactionType,
query.propertyType,
query.city,
query.district,
query.minPrice?.toString(),
query.maxPrice?.toString(),
query.minArea?.toString(),
query.maxArea?.toString(),
query.bedrooms?.toString(),
String(query.page),
String(query.limit),
);
try {
const cacheKey = CacheService.buildKey(
CachePrefix.SEARCH,
query.status,
query.transactionType,
query.propertyType,
query.city,
query.district,
query.minPrice?.toString(),
query.maxPrice?.toString(),
query.minArea?.toString(),
query.maxArea?.toString(),
query.bedrooms?.toString(),
String(query.page),
String(query.limit),
);
return this.cacheService.getOrSet(
cacheKey,
async () =>
this.listingRepo.search({
status: query.status,
transactionType: query.transactionType,
propertyType: query.propertyType,
city: query.city,
district: query.district,
minPrice: query.minPrice,
maxPrice: query.maxPrice,
minArea: query.minArea,
maxArea: query.maxArea,
bedrooms: query.bedrooms,
page: query.page,
limit: query.limit,
}),
CacheTTL.SEARCH_RESULTS,
'listing_search',
);
return this.cacheService.getOrSet(
cacheKey,
async () =>
this.listingRepo.search({
status: query.status,
transactionType: query.transactionType,
propertyType: query.propertyType,
city: query.city,
district: query.district,
minPrice: query.minPrice,
maxPrice: query.maxPrice,
minArea: query.minArea,
maxArea: query.maxArea,
bedrooms: query.bedrooms,
page: query.page,
limit: query.limit,
}),
CacheTTL.SEARCH_RESULTS,
'listing_search',
);
} catch (error) {
if (error instanceof DomainException) throw error;
this.logger.error(
`Failed to search listings: ${error instanceof Error ? error.message : error}`,
error instanceof Error ? error.stack : undefined,
this.constructor.name,
);
throw new InternalServerErrorException('Không thể tìm kiếm tin đăng bất động sản');
}
}
}