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, type PrismaService, type LoggerService } from '@modules/shared';
import { DomainException, NotFoundException, type PrismaService, type LoggerService } from '@modules/shared';
import { InquiryEntity } from '../../../domain/entities/inquiry.entity';
import { INQUIRY_REPOSITORY, type IInquiryRepository } from '../../../domain/repositories/inquiry.repository';
import { CreateInquiryCommand } from './create-inquiry.command';
@@ -22,38 +22,48 @@ export class CreateInquiryHandler implements ICommandHandler<CreateInquiryComman
) {}
async execute(command: CreateInquiryCommand): Promise<CreateInquiryResult> {
// Validate listing exists
const listing = await this.prisma.listing.findUnique({
where: { id: command.listingId },
select: { id: true },
});
if (!listing) {
throw new NotFoundException('Listing', command.listingId);
try {
// Validate listing exists
const listing = await this.prisma.listing.findUnique({
where: { id: command.listingId },
select: { id: true },
});
if (!listing) {
throw new NotFoundException('Listing', command.listingId);
}
const id = createId();
const inquiry = InquiryEntity.createNew(
id,
command.listingId,
command.userId,
command.message,
command.phone,
);
await this.inquiryRepo.save(inquiry);
// Publish domain events
const events = inquiry.clearDomainEvents();
for (const event of events) {
this.eventBus.publish(event);
}
this.logger.log(`Inquiry ${id} created by user ${command.userId} for listing ${command.listingId}`, 'CreateInquiryHandler');
return {
id,
listingId: command.listingId,
createdAt: inquiry.createdAt.toISOString(),
};
} catch (error) {
if (error instanceof DomainException) throw error;
this.logger.error(
`Failed to create inquiry: ${error instanceof Error ? error.message : String(error)}`,
error instanceof Error ? error.stack : undefined,
'CreateInquiryHandler',
);
throw new InternalServerErrorException('Lỗi khi tạo yêu cầu tư vấn');
}
const id = createId();
const inquiry = InquiryEntity.createNew(
id,
command.listingId,
command.userId,
command.message,
command.phone,
);
await this.inquiryRepo.save(inquiry);
// Publish domain events
const events = inquiry.clearDomainEvents();
for (const event of events) {
this.eventBus.publish(event);
}
this.logger.log(`Inquiry ${id} created by user ${command.userId} for listing ${command.listingId}`, 'CreateInquiryHandler');
return {
id,
listingId: command.listingId,
createdAt: inquiry.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 { INQUIRY_REPOSITORY, type IInquiryRepository } from '../../../domain/repositories/inquiry.repository';
import { MarkInquiryReadCommand } from './mark-inquiry-read.command';
@@ -14,37 +14,47 @@ export class MarkInquiryReadHandler implements ICommandHandler<MarkInquiryReadCo
) {}
async execute(command: MarkInquiryReadCommand): Promise<void> {
const inquiry = await this.inquiryRepo.findById(command.inquiryId);
if (!inquiry) {
throw new NotFoundException('Inquiry', command.inquiryId);
try {
const inquiry = await this.inquiryRepo.findById(command.inquiryId);
if (!inquiry) {
throw new NotFoundException('Inquiry', command.inquiryId);
}
// Verify the requesting user is the listing's agent
const listing = await this.prisma.listing.findUnique({
where: { id: inquiry.listingId },
select: { agentId: true },
});
if (!listing) {
throw new NotFoundException('Listing', inquiry.listingId);
}
const agent = await this.prisma.agent.findUnique({
where: { userId: command.agentUserId },
select: { id: true },
});
if (!agent || listing.agentId !== agent.id) {
throw new ForbiddenException('Bạn không có quyền đánh dấu yêu cầu tư vấn này');
}
inquiry.markAsRead();
await this.inquiryRepo.markAsRead(command.inquiryId);
// Publish domain events
const events = inquiry.clearDomainEvents();
for (const event of events) {
this.eventBus.publish(event);
}
this.logger.log(`Inquiry ${command.inquiryId} marked as read by agent ${command.agentUserId}`, 'MarkInquiryReadHandler');
} catch (error) {
if (error instanceof DomainException) throw error;
this.logger.error(
`Failed to mark inquiry as read: ${error instanceof Error ? error.message : String(error)}`,
error instanceof Error ? error.stack : undefined,
'MarkInquiryReadHandler',
);
throw new InternalServerErrorException('Lỗi khi đánh dấu đã đọc');
}
// Verify the requesting user is the listing's agent
const listing = await this.prisma.listing.findUnique({
where: { id: inquiry.listingId },
select: { agentId: true },
});
if (!listing) {
throw new NotFoundException('Listing', inquiry.listingId);
}
const agent = await this.prisma.agent.findUnique({
where: { userId: command.agentUserId },
select: { id: true },
});
if (!agent || listing.agentId !== agent.id) {
throw new ForbiddenException('Bạn không có quyền đánh dấu yêu cầu tư vấn này');
}
inquiry.markAsRead();
await this.inquiryRepo.markAsRead(command.inquiryId);
// Publish domain events
const events = inquiry.clearDomainEvents();
for (const event of events) {
this.eventBus.publish(event);
}
this.logger.log(`Inquiry ${command.inquiryId} marked as read by agent ${command.agentUserId}`, 'MarkInquiryReadHandler');
}
}

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 InquiryReadDto } from '../../../domain/repositories/inquiry-read.dto';
import { INQUIRY_REPOSITORY, type IInquiryRepository, type PaginatedResult } from '../../../domain/repositories/inquiry.repository';
import { GetInquiriesByAgentQuery } from './get-inquiries-by-agent.query';
@@ -10,21 +10,32 @@ export class GetInquiriesByAgentHandler implements IQueryHandler<GetInquiriesByA
constructor(
@Inject(INQUIRY_REPOSITORY) private readonly inquiryRepo: IInquiryRepository,
private readonly prisma: PrismaService,
private readonly logger: LoggerService,
) {}
async execute(query: GetInquiriesByAgentQuery): Promise<PaginatedResult<InquiryReadDto>> {
const agent = await this.prisma.agent.findUnique({
where: { userId: query.agentUserId },
select: { id: true },
});
if (!agent) {
throw new NotFoundException('Agent', query.agentUserId);
}
try {
const agent = await this.prisma.agent.findUnique({
where: { userId: query.agentUserId },
select: { id: true },
});
if (!agent) {
throw new NotFoundException('Agent', query.agentUserId);
}
return this.inquiryRepo.findByAgent(
agent.id,
query.page,
query.limit,
);
return this.inquiryRepo.findByAgent(
agent.id,
query.page,
query.limit,
);
} catch (error) {
if (error instanceof DomainException) throw error;
this.logger.error(
`Failed to get inquiries by agent: ${error instanceof Error ? error.message : String(error)}`,
error instanceof Error ? error.stack : undefined,
'GetInquiriesByAgentHandler',
);
throw new InternalServerErrorException('Lỗi khi lấy danh sách yêu cầu tư vấn');
}
}
}

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 { type InquiryReadDto } from '../../../domain/repositories/inquiry-read.dto';
import { INQUIRY_REPOSITORY, type IInquiryRepository, type PaginatedResult } from '../../../domain/repositories/inquiry.repository';
import { GetInquiriesByListingQuery } from './get-inquiries-by-listing.query';
@@ -8,13 +9,24 @@ import { GetInquiriesByListingQuery } from './get-inquiries-by-listing.query';
export class GetInquiriesByListingHandler implements IQueryHandler<GetInquiriesByListingQuery> {
constructor(
@Inject(INQUIRY_REPOSITORY) private readonly inquiryRepo: IInquiryRepository,
private readonly logger: LoggerService,
) {}
async execute(query: GetInquiriesByListingQuery): Promise<PaginatedResult<InquiryReadDto>> {
return this.inquiryRepo.findByListing(
query.listingId,
query.page,
query.limit,
);
try {
return await this.inquiryRepo.findByListing(
query.listingId,
query.page,
query.limit,
);
} catch (error) {
if (error instanceof DomainException) throw error;
this.logger.error(
`Failed to get inquiries by listing: ${error instanceof Error ? error.message : String(error)}`,
error instanceof Error ? error.stack : undefined,
'GetInquiriesByListingHandler',
);
throw new InternalServerErrorException('Lỗi khi lấy danh sách yêu cầu tư vấn');
}
}
}