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,11 +1,12 @@
import { Inject } from '@nestjs/common';
import { CommandHandler, type ICommandHandler } from '@nestjs/cqrs';
import { type PrismaService, type LoggerService } from '@modules/shared';
import { Inject, InternalServerErrorException } from '@nestjs/common';
import { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
import { DomainException, type LoggerService } from '@modules/shared';
import {
AGENT_REPOSITORY,
type IAgentRepository,
} from '../../../domain/repositories/agent.repository';
import { QualityScoreCalculator } from '../../../domain/services/quality-score.service';
import { QualityScore } from '../../../domain/value-objects/quality-score.vo';
import { RecalculateQualityScoreCommand } from './recalculate-quality-score.command';
@CommandHandler(RecalculateQualityScoreCommand)
@@ -15,70 +16,48 @@ export class RecalculateQualityScoreHandler
constructor(
@Inject(AGENT_REPOSITORY)
private readonly agentRepo: IAgentRepository,
private readonly prisma: PrismaService,
private readonly eventBus: EventBus,
private readonly logger: LoggerService,
) {}
async execute(command: RecalculateQualityScoreCommand): Promise<void> {
const agent = await this.agentRepo.findById(command.agentId);
if (!agent) {
this.logger.warn(`Agent ${command.agentId} not found, skipping recalculation`, 'RecalculateQualityScoreHandler');
return;
try {
const agent = await this.agentRepo.findById(command.agentId);
if (!agent) {
this.logger.warn(`Agent ${command.agentId} not found, skipping recalculation`, 'RecalculateQualityScoreHandler');
return;
}
const inputs = await this.agentRepo.getQualityScoreInputs(command.agentId);
const rawScore = QualityScoreCalculator.calculate(inputs);
const newScore = QualityScore.fromPersistence(rawScore);
agent.updateQualityScore(newScore);
await this.agentRepo.save(agent);
// Publish domain events
const events = agent.clearDomainEvents();
for (const event of events) {
this.eventBus.publish(event);
}
this.logger.log(
`Quality score recalculated for agent ${command.agentId}: ${rawScore} ` +
`(rating=${inputs.avgRating.toFixed(2)}, reviews=${inputs.totalReviews}, ` +
`conversion=${(inputs.conversionRate * 100).toFixed(1)}%, ` +
`activeListingRatio=${(inputs.activeListingRatio * 100).toFixed(1)}%)`,
'RecalculateQualityScoreHandler',
);
} catch (error) {
if (error instanceof DomainException) throw error;
this.logger.error(
`Failed to recalculate quality score for agent ${command.agentId}: ${error instanceof Error ? error.message : error}`,
error instanceof Error ? error.stack : undefined,
this.constructor.name,
);
throw new InternalServerErrorException('Không thể tính lại điểm chất lượng môi giới');
}
// Fetch review stats for this agent
const reviewStats = await this.prisma.review.aggregate({
where: { targetType: 'AGENT', targetId: command.agentId },
_avg: { rating: true },
_count: { rating: true },
});
const avgRating = reviewStats._avg.rating ?? 0;
const totalReviews = reviewStats._count.rating;
// Fetch lead conversion rate
const [totalLeads, convertedLeads] = await Promise.all([
this.prisma.lead.count({ where: { agentId: command.agentId } }),
this.prisma.lead.count({
where: { agentId: command.agentId, status: 'CONVERTED' },
}),
]);
const conversionRate = totalLeads > 0 ? convertedLeads / totalLeads : 0;
// Fetch listing activity ratio
const [totalListings, activeListings] = await Promise.all([
this.prisma.listing.count({
where: { agentId: command.agentId },
}),
this.prisma.listing.count({
where: { agentId: command.agentId, status: 'ACTIVE' },
}),
]);
const activeListingRatio =
totalListings > 0 ? activeListings / totalListings : 0;
// Fetch response time from agent record
const agentRecord = await this.prisma.agent.findUnique({
where: { id: command.agentId },
select: { responseTimeAvg: true },
});
const score = QualityScoreCalculator.calculate({
avgRating,
totalReviews,
responseTimeAvg: agentRecord?.responseTimeAvg ?? null,
conversionRate,
activeListingRatio,
});
await this.agentRepo.updateQualityScore(command.agentId, score);
this.logger.log(
`Quality score recalculated for agent ${command.agentId}: ${score} ` +
`(rating=${avgRating.toFixed(2)}, reviews=${totalReviews}, ` +
`conversion=${(conversionRate * 100).toFixed(1)}%, ` +
`activeListings=${activeListings}/${totalListings})`,
'RecalculateQualityScoreHandler',
);
}
}

View File

@@ -1,6 +1,6 @@
import { Inject } from '@nestjs/common';
import { Inject, InternalServerErrorException } from '@nestjs/common';
import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { NotFoundException } from '@modules/shared';
import { DomainException, NotFoundException, type LoggerService } from '@modules/shared';
import {
AGENT_REPOSITORY,
type AgentDashboardData,
@@ -15,14 +15,25 @@ export class GetAgentDashboardHandler
constructor(
@Inject(AGENT_REPOSITORY)
private readonly agentRepo: IAgentRepository,
private readonly logger: LoggerService,
) {}
async execute(query: GetAgentDashboardQuery): Promise<AgentDashboardData> {
const agent = await this.agentRepo.findByUserId(query.userId);
if (!agent) {
throw new NotFoundException('Không tìm thấy thông tin môi giới');
}
try {
const agent = await this.agentRepo.findByUserId(query.userId);
if (!agent) {
throw new NotFoundException('Không tìm thấy thông tin môi giới');
}
return this.agentRepo.getDashboard(agent.id);
return this.agentRepo.getDashboard(agent.id);
} catch (error) {
if (error instanceof DomainException) throw error;
this.logger.error(
`Failed to get agent dashboard: ${error instanceof Error ? error.message : error}`,
error instanceof Error ? error.stack : undefined,
this.constructor.name,
);
throw new InternalServerErrorException('Không thể tải bảng điều khiển môi giới');
}
}
}

View File

@@ -1,5 +1,6 @@
import { Inject } from '@nestjs/common';
import { Inject, InternalServerErrorException } from '@nestjs/common';
import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { DomainException, type LoggerService } from '@modules/shared';
import {
AGENT_REPOSITORY,
type AgentPublicProfileData,
@@ -14,11 +15,22 @@ export class GetAgentPublicProfileHandler
constructor(
@Inject(AGENT_REPOSITORY)
private readonly agentRepo: IAgentRepository,
private readonly logger: LoggerService,
) {}
async execute(
query: GetAgentPublicProfileQuery,
): Promise<AgentPublicProfileData | null> {
return this.agentRepo.getPublicProfile(query.agentId);
try {
return this.agentRepo.getPublicProfile(query.agentId);
} catch (error) {
if (error instanceof DomainException) throw error;
this.logger.error(
`Failed to get agent public profile: ${error instanceof Error ? error.message : error}`,
error instanceof Error ? error.stack : undefined,
this.constructor.name,
);
throw new InternalServerErrorException('Không thể tải hồ sơ công khai của môi giới');
}
}
}