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,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { type PrismaService } from '@modules/shared';
import { type PrismaService, DomainException, LoggerService } from '@modules/shared';
import { GetAgentByUserIdQuery } from './get-agent-by-user-id.query';
export interface AgentDto {
@@ -20,27 +20,40 @@ export interface AgentDto {
@Injectable()
@QueryHandler(GetAgentByUserIdQuery)
export class GetAgentByUserIdHandler implements IQueryHandler<GetAgentByUserIdQuery> {
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly logger: LoggerService,
) {}
async execute(query: GetAgentByUserIdQuery): Promise<AgentDto | null> {
const agent = await this.prisma.agent.findUnique({
where: { userId: query.userId },
});
try {
const agent = await this.prisma.agent.findUnique({
where: { userId: query.userId },
});
if (!agent) return null;
if (!agent) return null;
return {
id: agent.id,
userId: agent.userId,
licenseNumber: agent.licenseNumber,
agency: agent.agency,
qualityScore: agent.qualityScore,
totalDeals: agent.totalDeals,
responseTimeAvg: agent.responseTimeAvg,
bio: agent.bio,
serviceAreas: agent.serviceAreas,
isVerified: agent.isVerified,
createdAt: agent.createdAt,
};
return {
id: agent.id,
userId: agent.userId,
licenseNumber: agent.licenseNumber,
agency: agent.agency,
qualityScore: agent.qualityScore,
totalDeals: agent.totalDeals,
responseTimeAvg: agent.responseTimeAvg,
bio: agent.bio,
serviceAreas: agent.serviceAreas,
isVerified: agent.isVerified,
createdAt: agent.createdAt,
};
} catch (error) {
if (error instanceof DomainException) throw error;
this.logger.error(
`Failed to get agent by user ID: ${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 đại lý');
}
}
}

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, CacheService, CachePrefix, CacheTTL } from '@modules/shared';
import { DomainException, LoggerService, NotFoundException, CacheService, CachePrefix, CacheTTL } from '@modules/shared';
import { USER_REPOSITORY, type IUserRepository } from '../../../domain/repositories/user.repository';
import { GetProfileQuery } from './get-profile.query';
@@ -21,33 +21,44 @@ export class GetProfileHandler implements IQueryHandler<GetProfileQuery> {
constructor(
@Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository,
private readonly cache: CacheService,
private readonly logger: LoggerService,
) {}
async execute(query: GetProfileQuery): Promise<UserProfileDto> {
const cacheKey = CacheService.buildKey(CachePrefix.USER_PROFILE, query.userId);
try {
const cacheKey = CacheService.buildKey(CachePrefix.USER_PROFILE, query.userId);
return this.cache.getOrSet(
cacheKey,
async () => {
const user = await this.userRepo.findById(query.userId);
if (!user) {
throw new NotFoundException('Người dùng', query.userId);
}
return this.cache.getOrSet(
cacheKey,
async () => {
const user = await this.userRepo.findById(query.userId);
if (!user) {
throw new NotFoundException('Người dùng', query.userId);
}
return {
id: user.id,
email: user.email?.value ?? null,
phone: user.phone.value,
fullName: user.fullName,
avatarUrl: user.avatarUrl,
role: user.role,
kycStatus: user.kycStatus,
isActive: user.isActive,
createdAt: user.createdAt,
};
},
CacheTTL.USER_PROFILE,
'user_profile',
);
return {
id: user.id,
email: user.email?.value ?? null,
phone: user.phone.value,
fullName: user.fullName,
avatarUrl: user.avatarUrl,
role: user.role,
kycStatus: user.kycStatus,
isActive: user.isActive,
createdAt: user.createdAt,
};
},
CacheTTL.USER_PROFILE,
'user_profile',
);
} catch (error) {
if (error instanceof DomainException) throw error;
this.logger.error(
`Failed to get user profile: ${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 hồ sơ người dùng');
}
}
}