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:
@@ -1,5 +1,6 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
|
||||
import { DomainException, type LoggerService } from '@modules/shared';
|
||||
import {
|
||||
MARKET_INDEX_REPOSITORY,
|
||||
type IMarketIndexRepository,
|
||||
@@ -18,20 +19,31 @@ export interface GenerateReportResult {
|
||||
export class GenerateReportHandler implements ICommandHandler<GenerateReportCommand> {
|
||||
constructor(
|
||||
@Inject(MARKET_INDEX_REPOSITORY) private readonly marketIndexRepo: IMarketIndexRepository,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async execute(command: GenerateReportCommand): Promise<GenerateReportResult> {
|
||||
const data = await this.marketIndexRepo.getMarketReport(
|
||||
command.city,
|
||||
command.period,
|
||||
command.propertyType,
|
||||
);
|
||||
try {
|
||||
const data = await this.marketIndexRepo.getMarketReport(
|
||||
command.city,
|
||||
command.period,
|
||||
command.propertyType,
|
||||
);
|
||||
|
||||
return {
|
||||
city: command.city,
|
||||
period: command.period,
|
||||
data,
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
return {
|
||||
city: command.city,
|
||||
period: command.period,
|
||||
data,
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof DomainException) throw error;
|
||||
this.logger.error(
|
||||
`Failed to tạo báo cáo thị trường: ${error instanceof Error ? error.message : error}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
this.constructor.name,
|
||||
);
|
||||
throw new InternalServerErrorException('Không thể tạo báo cáo thị trường. Vui lòng thử lại sau.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { InternalServerErrorException } from '@nestjs/common';
|
||||
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
|
||||
import { type LoggerService } from '@modules/shared';
|
||||
import { DomainException, type LoggerService } from '@modules/shared';
|
||||
import { TrackEventCommand } from './track-event.command';
|
||||
|
||||
export interface TrackEventResult {
|
||||
@@ -12,14 +13,24 @@ export class TrackEventHandler implements ICommandHandler<TrackEventCommand> {
|
||||
constructor(private readonly logger: LoggerService) {}
|
||||
|
||||
async execute(command: TrackEventCommand): Promise<TrackEventResult> {
|
||||
this.logger.log(
|
||||
`Analytics event: ${command.eventType} on ${command.entityType}:${command.entityId}`,
|
||||
'TrackEventHandler',
|
||||
);
|
||||
try {
|
||||
this.logger.log(
|
||||
`Analytics event: ${command.eventType} on ${command.entityType}:${command.entityId}`,
|
||||
'TrackEventHandler',
|
||||
);
|
||||
|
||||
return {
|
||||
tracked: true,
|
||||
eventType: command.eventType,
|
||||
};
|
||||
return {
|
||||
tracked: true,
|
||||
eventType: command.eventType,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof DomainException) throw error;
|
||||
this.logger.error(
|
||||
`Failed to ghi nhận sự kiện phân tích: ${error instanceof Error ? error.message : error}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
this.constructor.name,
|
||||
);
|
||||
throw new InternalServerErrorException('Không thể ghi nhận sự kiện phân tích. Vui lòng thử lại sau.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
|
||||
import { type CacheService, CachePrefix } from '@modules/shared';
|
||||
import { DomainException, type CacheService, CachePrefix, type LoggerService } from '@modules/shared';
|
||||
import { MarketIndexEntity } from '../../../domain/entities/market-index.entity';
|
||||
import {
|
||||
MARKET_INDEX_REPOSITORY,
|
||||
@@ -18,18 +18,40 @@ export class UpdateMarketIndexHandler implements ICommandHandler<UpdateMarketInd
|
||||
constructor(
|
||||
@Inject(MARKET_INDEX_REPOSITORY) private readonly marketIndexRepo: IMarketIndexRepository,
|
||||
private readonly cache: CacheService,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async execute(command: UpdateMarketIndexCommand): Promise<UpdateMarketIndexResult> {
|
||||
const existing = await this.marketIndexRepo.findByKey(
|
||||
command.district,
|
||||
command.city,
|
||||
command.propertyType,
|
||||
command.period,
|
||||
);
|
||||
try {
|
||||
const existing = await this.marketIndexRepo.findByKey(
|
||||
command.district,
|
||||
command.city,
|
||||
command.propertyType,
|
||||
command.period,
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
existing.updateMetrics(
|
||||
if (existing) {
|
||||
existing.updateMetrics(
|
||||
command.medianPrice,
|
||||
command.avgPriceM2,
|
||||
command.totalListings,
|
||||
command.daysOnMarket,
|
||||
command.inventoryLevel,
|
||||
command.absorptionRate,
|
||||
command.yoyChange,
|
||||
);
|
||||
await this.marketIndexRepo.update(existing);
|
||||
await this.invalidateMarketCaches();
|
||||
return { id: existing.id, created: false };
|
||||
}
|
||||
|
||||
const id = crypto.randomUUID();
|
||||
const entity = MarketIndexEntity.createNew(
|
||||
id,
|
||||
command.district,
|
||||
command.city,
|
||||
command.propertyType,
|
||||
command.period,
|
||||
command.medianPrice,
|
||||
command.avgPriceM2,
|
||||
command.totalListings,
|
||||
@@ -38,32 +60,21 @@ export class UpdateMarketIndexHandler implements ICommandHandler<UpdateMarketInd
|
||||
command.absorptionRate,
|
||||
command.yoyChange,
|
||||
);
|
||||
await this.marketIndexRepo.update(existing);
|
||||
|
||||
await this.marketIndexRepo.save(entity);
|
||||
|
||||
await this.invalidateMarketCaches();
|
||||
return { id: existing.id, created: false };
|
||||
|
||||
return { id, created: true };
|
||||
} catch (error) {
|
||||
if (error instanceof DomainException) throw error;
|
||||
this.logger.error(
|
||||
`Failed to cập nhật chỉ số thị trường: ${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 chỉ số thị trường. Vui lòng thử lại sau.');
|
||||
}
|
||||
|
||||
const id = crypto.randomUUID();
|
||||
const entity = MarketIndexEntity.createNew(
|
||||
id,
|
||||
command.district,
|
||||
command.city,
|
||||
command.propertyType,
|
||||
command.period,
|
||||
command.medianPrice,
|
||||
command.avgPriceM2,
|
||||
command.totalListings,
|
||||
command.daysOnMarket,
|
||||
command.inventoryLevel,
|
||||
command.absorptionRate,
|
||||
command.yoyChange,
|
||||
);
|
||||
|
||||
await this.marketIndexRepo.save(entity);
|
||||
|
||||
await this.invalidateMarketCaches();
|
||||
|
||||
return { id, created: true };
|
||||
}
|
||||
|
||||
private async invalidateMarketCaches(): Promise<void> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
|
||||
import { type CacheService, CachePrefix, CacheTTL, Cacheable } from '@modules/shared';
|
||||
import { DomainException, type CacheService, CachePrefix, CacheTTL, Cacheable, type LoggerService } from '@modules/shared';
|
||||
import {
|
||||
MARKET_INDEX_REPOSITORY,
|
||||
type IMarketIndexRepository,
|
||||
@@ -19,6 +19,7 @@ export class GetDistrictStatsHandler implements IQueryHandler<GetDistrictStatsQu
|
||||
constructor(
|
||||
@Inject(MARKET_INDEX_REPOSITORY) private readonly marketIndexRepo: IMarketIndexRepository,
|
||||
private readonly cacheService: CacheService,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
@Cacheable({
|
||||
@@ -31,7 +32,17 @@ export class GetDistrictStatsHandler implements IQueryHandler<GetDistrictStatsQu
|
||||
},
|
||||
})
|
||||
async execute(query: GetDistrictStatsQuery): Promise<DistrictStatsDto> {
|
||||
const districts = await this.marketIndexRepo.getDistrictStats(query.city, query.period);
|
||||
return { city: query.city, period: query.period, districts };
|
||||
try {
|
||||
const districts = await this.marketIndexRepo.getDistrictStats(query.city, query.period);
|
||||
return { city: query.city, period: query.period, districts };
|
||||
} catch (error) {
|
||||
if (error instanceof DomainException) throw error;
|
||||
this.logger.error(
|
||||
`Failed to truy vấn thống kê quận/huyện: ${error instanceof Error ? error.message : error}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
this.constructor.name,
|
||||
);
|
||||
throw new InternalServerErrorException('Không thể truy vấn thống kê quận/huyện. Vui lòng thử lại sau.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
MARKET_INDEX_REPOSITORY,
|
||||
type IMarketIndexRepository,
|
||||
@@ -19,19 +19,30 @@ export class GetHeatmapHandler implements IQueryHandler<GetHeatmapQuery> {
|
||||
constructor(
|
||||
@Inject(MARKET_INDEX_REPOSITORY) private readonly marketIndexRepo: IMarketIndexRepository,
|
||||
private readonly cache: CacheService,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async execute(query: GetHeatmapQuery): Promise<HeatmapDto> {
|
||||
const cacheKey = CacheService.buildKey(CachePrefix.MARKET_HEATMAP, query.city, query.period);
|
||||
try {
|
||||
const cacheKey = CacheService.buildKey(CachePrefix.MARKET_HEATMAP, query.city, query.period);
|
||||
|
||||
return this.cache.getOrSet(
|
||||
cacheKey,
|
||||
async () => {
|
||||
const dataPoints = await this.marketIndexRepo.getHeatmap(query.city, query.period);
|
||||
return { city: query.city, period: query.period, dataPoints };
|
||||
},
|
||||
CacheTTL.HEATMAP,
|
||||
'heatmap',
|
||||
);
|
||||
return this.cache.getOrSet(
|
||||
cacheKey,
|
||||
async () => {
|
||||
const dataPoints = await this.marketIndexRepo.getHeatmap(query.city, query.period);
|
||||
return { city: query.city, period: query.period, dataPoints };
|
||||
},
|
||||
CacheTTL.HEATMAP,
|
||||
'heatmap',
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof DomainException) throw error;
|
||||
this.logger.error(
|
||||
`Failed to truy vấn dữ liệu bản đồ nhiệt: ${error instanceof Error ? error.message : error}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
this.constructor.name,
|
||||
);
|
||||
throw new InternalServerErrorException('Không thể truy vấn dữ liệu bản đồ nhiệt. Vui lòng thử lại sau.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
MARKET_INDEX_REPOSITORY,
|
||||
type IMarketIndexRepository,
|
||||
@@ -19,23 +19,34 @@ export class GetMarketReportHandler implements IQueryHandler<GetMarketReportQuer
|
||||
constructor(
|
||||
@Inject(MARKET_INDEX_REPOSITORY) private readonly marketIndexRepo: IMarketIndexRepository,
|
||||
private readonly cache: CacheService,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async execute(query: GetMarketReportQuery): Promise<MarketReportDto> {
|
||||
const cacheKey = CacheService.buildKey(CachePrefix.MARKET_REPORT, query.city, query.period, query.propertyType);
|
||||
try {
|
||||
const cacheKey = CacheService.buildKey(CachePrefix.MARKET_REPORT, query.city, query.period, query.propertyType);
|
||||
|
||||
return this.cache.getOrSet(
|
||||
cacheKey,
|
||||
async () => {
|
||||
const districts = await this.marketIndexRepo.getMarketReport(
|
||||
query.city,
|
||||
query.period,
|
||||
query.propertyType,
|
||||
);
|
||||
return { city: query.city, period: query.period, districts };
|
||||
},
|
||||
CacheTTL.MARKET_REPORT,
|
||||
'market_report',
|
||||
);
|
||||
return this.cache.getOrSet(
|
||||
cacheKey,
|
||||
async () => {
|
||||
const districts = await this.marketIndexRepo.getMarketReport(
|
||||
query.city,
|
||||
query.period,
|
||||
query.propertyType,
|
||||
);
|
||||
return { city: query.city, period: query.period, districts };
|
||||
},
|
||||
CacheTTL.MARKET_REPORT,
|
||||
'market_report',
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof DomainException) throw error;
|
||||
this.logger.error(
|
||||
`Failed to truy vấn báo cáo thị trường: ${error instanceof Error ? error.message : error}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
this.constructor.name,
|
||||
);
|
||||
throw new InternalServerErrorException('Không thể truy vấn báo cáo thị trường. Vui lòng thử lại sau.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
MARKET_INDEX_REPOSITORY,
|
||||
type IMarketIndexRepository,
|
||||
@@ -20,30 +20,41 @@ export class GetPriceTrendHandler implements IQueryHandler<GetPriceTrendQuery> {
|
||||
constructor(
|
||||
@Inject(MARKET_INDEX_REPOSITORY) private readonly marketIndexRepo: IMarketIndexRepository,
|
||||
private readonly cache: CacheService,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async execute(query: GetPriceTrendQuery): Promise<PriceTrendDto> {
|
||||
const cacheKey = CacheService.buildKey(
|
||||
CachePrefix.MARKET_TREND,
|
||||
query.district,
|
||||
query.city,
|
||||
query.propertyType,
|
||||
query.periods?.join(','),
|
||||
);
|
||||
try {
|
||||
const cacheKey = CacheService.buildKey(
|
||||
CachePrefix.MARKET_TREND,
|
||||
query.district,
|
||||
query.city,
|
||||
query.propertyType,
|
||||
query.periods?.join(','),
|
||||
);
|
||||
|
||||
return this.cache.getOrSet(
|
||||
cacheKey,
|
||||
async () => {
|
||||
const trend = await this.marketIndexRepo.getPriceTrend(
|
||||
query.district,
|
||||
query.city,
|
||||
query.propertyType,
|
||||
query.periods,
|
||||
);
|
||||
return { district: query.district, city: query.city, propertyType: query.propertyType, trend };
|
||||
},
|
||||
CacheTTL.MARKET_DATA,
|
||||
'price_trend',
|
||||
);
|
||||
return this.cache.getOrSet(
|
||||
cacheKey,
|
||||
async () => {
|
||||
const trend = await this.marketIndexRepo.getPriceTrend(
|
||||
query.district,
|
||||
query.city,
|
||||
query.propertyType,
|
||||
query.periods,
|
||||
);
|
||||
return { district: query.district, city: query.city, propertyType: query.propertyType, trend };
|
||||
},
|
||||
CacheTTL.MARKET_DATA,
|
||||
'price_trend',
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof DomainException) throw error;
|
||||
this.logger.error(
|
||||
`Failed to truy vấn xu hướng giá: ${error instanceof Error ? error.message : error}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
this.constructor.name,
|
||||
);
|
||||
throw new InternalServerErrorException('Không thể truy vấn xu hướng giá. Vui lòng thử lại sau.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
AVM_SERVICE,
|
||||
type IAVMService,
|
||||
@@ -15,31 +15,42 @@ export class GetValuationHandler implements IQueryHandler<GetValuationQuery> {
|
||||
constructor(
|
||||
@Inject(AVM_SERVICE) private readonly avmService: IAVMService,
|
||||
private readonly cache: CacheService,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async execute(query: GetValuationQuery): Promise<ValuationDto> {
|
||||
const cacheKey = CacheService.buildKey(
|
||||
CachePrefix.VALUATION,
|
||||
query.propertyId ?? '',
|
||||
query.latitude?.toString(),
|
||||
query.longitude?.toString(),
|
||||
query.areaM2?.toString(),
|
||||
query.propertyType,
|
||||
);
|
||||
try {
|
||||
const cacheKey = CacheService.buildKey(
|
||||
CachePrefix.VALUATION,
|
||||
query.propertyId ?? '',
|
||||
query.latitude?.toString(),
|
||||
query.longitude?.toString(),
|
||||
query.areaM2?.toString(),
|
||||
query.propertyType,
|
||||
);
|
||||
|
||||
return this.cache.getOrSet(
|
||||
cacheKey,
|
||||
async () => {
|
||||
return this.avmService.estimateValue({
|
||||
propertyId: query.propertyId,
|
||||
latitude: query.latitude,
|
||||
longitude: query.longitude,
|
||||
areaM2: query.areaM2,
|
||||
propertyType: query.propertyType,
|
||||
});
|
||||
},
|
||||
CacheTTL.MARKET_DATA,
|
||||
'valuation',
|
||||
);
|
||||
return this.cache.getOrSet(
|
||||
cacheKey,
|
||||
async () => {
|
||||
return this.avmService.estimateValue({
|
||||
propertyId: query.propertyId,
|
||||
latitude: query.latitude,
|
||||
longitude: query.longitude,
|
||||
areaM2: query.areaM2,
|
||||
propertyType: query.propertyType,
|
||||
});
|
||||
},
|
||||
CacheTTL.MARKET_DATA,
|
||||
'valuation',
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof DomainException) throw error;
|
||||
this.logger.error(
|
||||
`Failed to định giá bất động sản: ${error instanceof Error ? error.message : error}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
this.constructor.name,
|
||||
);
|
||||
throw new InternalServerErrorException('Không thể định giá bất động sản. Vui lòng thử lại sau.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user