fix(api): add error handling to 29 CQRS handlers in admin, inquiries, leads, reviews

Add standardized try-catch error handling pattern to all command and
query handlers in the four priority modules:
- admin (15 handlers): commands + queries, added LoggerService injection
- inquiries (4 handlers): commands + queries
- leads (5 handlers): commands + queries
- reviews (5 handlers): commands + queries

Each handler now:
- Wraps execute() in try-catch
- Re-throws DomainException subclasses (NotFoundException, etc.)
- Logs infrastructure errors via LoggerService
- Throws InternalServerErrorException for unexpected failures

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-11 19:35:21 +07:00
parent c0537ed535
commit 2da333a95b
29 changed files with 897 additions and 575 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 { NotFoundException, ValidationException, type PrismaService, type LoggerService } from '@modules/shared';
import { DomainException, NotFoundException, ValidationException, type PrismaService, type LoggerService } from '@modules/shared';
import { LeadEntity } from '../../../domain/entities/lead.entity';
import { LEAD_REPOSITORY, type ILeadRepository } from '../../../domain/repositories/lead.repository';
import { LeadScore } from '../../../domain/value-objects/lead-score.vo';
@@ -23,50 +23,60 @@ export class CreateLeadHandler implements ICommandHandler<CreateLeadCommand> {
) {}
async execute(command: CreateLeadCommand): Promise<CreateLeadResult> {
// Look up agent by userId
const agent = await this.prisma.agent.findUnique({
where: { userId: command.agentUserId },
});
if (!agent) {
throw new NotFoundException('Agent', command.agentUserId);
}
// Validate score value object
let score: LeadScore | null = null;
if (command.score !== null && command.score !== undefined) {
const scoreResult = LeadScore.create(command.score);
if (scoreResult.isErr) {
throw new ValidationException(scoreResult.unwrapErr());
try {
// Look up agent by userId
const agent = await this.prisma.agent.findUnique({
where: { userId: command.agentUserId },
});
if (!agent) {
throw new NotFoundException('Agent', command.agentUserId);
}
score = scoreResult.unwrap();
// Validate score value object
let score: LeadScore | null = null;
if (command.score !== null && command.score !== undefined) {
const scoreResult = LeadScore.create(command.score);
if (scoreResult.isErr) {
throw new ValidationException(scoreResult.unwrapErr());
}
score = scoreResult.unwrap();
}
const id = createId();
const lead = LeadEntity.createNew(
id,
agent.id,
command.name,
command.phone,
command.email,
command.source,
score,
command.notes ?? null,
);
await this.leadRepo.save(lead);
// Publish domain events
const events = lead.clearDomainEvents();
for (const event of events) {
this.eventBus.publish(event);
}
this.logger.log(`Lead ${id} created by agent ${agent.id}`, 'CreateLeadHandler');
return {
id,
status: lead.status,
createdAt: lead.createdAt.toISOString(),
};
} catch (error) {
if (error instanceof DomainException) throw error;
this.logger.error(
`Failed to create lead: ${error instanceof Error ? error.message : String(error)}`,
error instanceof Error ? error.stack : undefined,
'CreateLeadHandler',
);
throw new InternalServerErrorException('Lỗi khi tạo lead');
}
const id = createId();
const lead = LeadEntity.createNew(
id,
agent.id,
command.name,
command.phone,
command.email,
command.source,
score,
command.notes ?? null,
);
await this.leadRepo.save(lead);
// Publish domain events
const events = lead.clearDomainEvents();
for (const event of events) {
this.eventBus.publish(event);
}
this.logger.log(`Lead ${id} created by agent ${agent.id}`, 'CreateLeadHandler');
return {
id,
status: lead.status,
createdAt: lead.createdAt.toISOString(),
};
}
}

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 { ForbiddenException, NotFoundException, type PrismaService, type LoggerService } from '@modules/shared';
import { DomainException, ForbiddenException, NotFoundException, type PrismaService, type LoggerService } from '@modules/shared';
import { LEAD_REPOSITORY, type ILeadRepository } from '../../../domain/repositories/lead.repository';
import { DeleteLeadCommand } from './delete-lead.command';
@@ -14,26 +14,36 @@ export class DeleteLeadHandler implements ICommandHandler<DeleteLeadCommand> {
) {}
async execute(command: DeleteLeadCommand): Promise<void> {
// Look up agent by userId
const agent = await this.prisma.agent.findUnique({
where: { userId: command.agentUserId },
});
if (!agent) {
throw new NotFoundException('Agent', command.agentUserId);
try {
// Look up agent by userId
const agent = await this.prisma.agent.findUnique({
where: { userId: command.agentUserId },
});
if (!agent) {
throw new NotFoundException('Agent', command.agentUserId);
}
const lead = await this.leadRepo.findById(command.leadId);
if (!lead) {
throw new NotFoundException('Lead', command.leadId);
}
// Verify agent ownership
if (lead.agentId !== agent.id) {
throw new ForbiddenException('Bạn chỉ có thể xóa lead của chính mình');
}
await this.leadRepo.delete(command.leadId);
this.logger.log(`Lead ${command.leadId} deleted by agent ${agent.id}`, 'DeleteLeadHandler');
} catch (error) {
if (error instanceof DomainException) throw error;
this.logger.error(
`Failed to delete lead: ${error instanceof Error ? error.message : String(error)}`,
error instanceof Error ? error.stack : undefined,
'DeleteLeadHandler',
);
throw new InternalServerErrorException('Lỗi khi xóa lead');
}
const lead = await this.leadRepo.findById(command.leadId);
if (!lead) {
throw new NotFoundException('Lead', command.leadId);
}
// Verify agent ownership
if (lead.agentId !== agent.id) {
throw new ForbiddenException('Bạn chỉ có thể xóa lead của chính mình');
}
await this.leadRepo.delete(command.leadId);
this.logger.log(`Lead ${command.leadId} deleted by agent ${agent.id}`, 'DeleteLeadHandler');
}
}

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 { ForbiddenException, NotFoundException, type PrismaService, type LoggerService } from '@modules/shared';
import { DomainException, ForbiddenException, NotFoundException, type PrismaService, type LoggerService } from '@modules/shared';
import { type LeadStatus } from '../../../domain/entities/lead.entity';
import { LEAD_REPOSITORY, type ILeadRepository } from '../../../domain/repositories/lead.repository';
import { UpdateLeadStatusCommand } from './update-lead-status.command';
@@ -15,33 +15,43 @@ export class UpdateLeadStatusHandler implements ICommandHandler<UpdateLeadStatus
) {}
async execute(command: UpdateLeadStatusCommand): Promise<void> {
// Look up agent by userId
const agent = await this.prisma.agent.findUnique({
where: { userId: command.agentUserId },
});
if (!agent) {
throw new NotFoundException('Agent', command.agentUserId);
try {
// Look up agent by userId
const agent = await this.prisma.agent.findUnique({
where: { userId: command.agentUserId },
});
if (!agent) {
throw new NotFoundException('Agent', command.agentUserId);
}
const lead = await this.leadRepo.findById(command.leadId);
if (!lead) {
throw new NotFoundException('Lead', command.leadId);
}
// Verify agent ownership
if (lead.agentId !== agent.id) {
throw new ForbiddenException('Bạn chỉ có thể cập nhật lead của chính mình');
}
lead.updateStatus(command.newStatus as LeadStatus);
await this.leadRepo.update(lead);
// Publish domain events
const events = lead.clearDomainEvents();
for (const event of events) {
this.eventBus.publish(event);
}
this.logger.log(`Lead ${command.leadId} status updated to ${command.newStatus} by agent ${agent.id}`, 'UpdateLeadStatusHandler');
} catch (error) {
if (error instanceof DomainException) throw error;
this.logger.error(
`Failed to update lead status: ${error instanceof Error ? error.message : String(error)}`,
error instanceof Error ? error.stack : undefined,
'UpdateLeadStatusHandler',
);
throw new InternalServerErrorException('Lỗi khi cập nhật trạng thái lead');
}
const lead = await this.leadRepo.findById(command.leadId);
if (!lead) {
throw new NotFoundException('Lead', command.leadId);
}
// Verify agent ownership
if (lead.agentId !== agent.id) {
throw new ForbiddenException('Bạn chỉ có thể cập nhật lead của chính mình');
}
lead.updateStatus(command.newStatus as LeadStatus);
await this.leadRepo.update(lead);
// Publish domain events
const events = lead.clearDomainEvents();
for (const event of events) {
this.eventBus.publish(event);
}
this.logger.log(`Lead ${command.leadId} status updated to ${command.newStatus} by agent ${agent.id}`, 'UpdateLeadStatusHandler');
}
}

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, type PrismaService } from '@modules/shared';
import { DomainException, NotFoundException, type PrismaService, type LoggerService } from '@modules/shared';
import { LEAD_REPOSITORY, type ILeadRepository, type LeadStatsData } from '../../../domain/repositories/lead.repository';
import { GetLeadStatsQuery } from './get-lead-stats.query';
@@ -9,17 +9,28 @@ export class GetLeadStatsHandler implements IQueryHandler<GetLeadStatsQuery> {
constructor(
@Inject(LEAD_REPOSITORY) private readonly leadRepo: ILeadRepository,
private readonly prisma: PrismaService,
private readonly logger: LoggerService,
) {}
async execute(query: GetLeadStatsQuery): Promise<LeadStatsData> {
// Look up agent by userId
const agent = await this.prisma.agent.findUnique({
where: { userId: query.agentUserId },
});
if (!agent) {
throw new NotFoundException('Agent', query.agentUserId);
}
try {
// Look up agent by userId
const agent = await this.prisma.agent.findUnique({
where: { userId: query.agentUserId },
});
if (!agent) {
throw new NotFoundException('Agent', query.agentUserId);
}
return this.leadRepo.getStatsByAgent(agent.id);
return this.leadRepo.getStatsByAgent(agent.id);
} catch (error) {
if (error instanceof DomainException) throw error;
this.logger.error(
`Failed to get lead stats: ${error instanceof Error ? error.message : String(error)}`,
error instanceof Error ? error.stack : undefined,
'GetLeadStatsHandler',
);
throw new InternalServerErrorException('Lỗi khi lấy thống kê lead');
}
}
}

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, type PrismaService } from '@modules/shared';
import { DomainException, NotFoundException, type PrismaService, type LoggerService } from '@modules/shared';
import { type LeadReadDto } from '../../../domain/repositories/lead-read.dto';
import { LEAD_REPOSITORY, type ILeadRepository, type PaginatedResult } from '../../../domain/repositories/lead.repository';
import { GetLeadsByAgentQuery } from './get-leads-by-agent.query';
@@ -10,22 +10,33 @@ export class GetLeadsByAgentHandler implements IQueryHandler<GetLeadsByAgentQuer
constructor(
@Inject(LEAD_REPOSITORY) private readonly leadRepo: ILeadRepository,
private readonly prisma: PrismaService,
private readonly logger: LoggerService,
) {}
async execute(query: GetLeadsByAgentQuery): Promise<PaginatedResult<LeadReadDto>> {
// Look up agent by userId
const agent = await this.prisma.agent.findUnique({
where: { userId: query.agentUserId },
});
if (!agent) {
throw new NotFoundException('Agent', query.agentUserId);
}
try {
// Look up agent by userId
const agent = await this.prisma.agent.findUnique({
where: { userId: query.agentUserId },
});
if (!agent) {
throw new NotFoundException('Agent', query.agentUserId);
}
return this.leadRepo.findByAgent(
agent.id,
query.status,
query.page,
query.limit,
);
return this.leadRepo.findByAgent(
agent.id,
query.status,
query.page,
query.limit,
);
} catch (error) {
if (error instanceof DomainException) throw error;
this.logger.error(
`Failed to get leads by agent: ${error instanceof Error ? error.message : String(error)}`,
error instanceof Error ? error.stack : undefined,
'GetLeadsByAgentHandler',
);
throw new InternalServerErrorException('Lỗi khi lấy danh sách lead');
}
}
}