From 2da333a95b300fd780a9b07242a2523690b88ba9 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Sat, 11 Apr 2026 19:35:21 +0700 Subject: [PATCH] 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 --- .../adjust-subscription.handler.ts | 91 ++++++++------ .../approve-kyc/approve-kyc.handler.ts | 53 ++++---- .../approve-listing.handler.ts | 67 +++++----- .../commands/ban-user/ban-user.handler.ts | 83 +++++++------ .../bulk-moderate-listings.handler.ts | 115 ++++++++++-------- .../commands/reject-kyc/reject-kyc.handler.ts | 53 ++++---- .../reject-listing/reject-listing.handler.ts | 57 +++++---- .../update-user-status.handler.ts | 75 +++++++----- .../get-audit-logs/get-audit-logs.handler.ts | 34 ++++-- .../get-dashboard-stats.handler.ts | 16 ++- .../get-kyc-queue/get-kyc-queue.handler.ts | 16 ++- .../get-moderation-queue.handler.ts | 16 ++- .../get-revenue-stats.handler.ts | 16 ++- .../get-user-detail.handler.ts | 23 +++- .../queries/get-users/get-users.handler.ts | 28 +++-- .../create-inquiry/create-inquiry.handler.ts | 78 ++++++------ .../mark-inquiry-read.handler.ts | 76 +++++++----- .../get-inquiries-by-agent.handler.ts | 39 +++--- .../get-inquiries-by-listing.handler.ts | 24 +++- .../create-lead/create-lead.handler.ts | 100 ++++++++------- .../delete-lead/delete-lead.handler.ts | 54 ++++---- .../update-lead-status.handler.ts | 68 ++++++----- .../get-lead-stats/get-lead-stats.handler.ts | 31 +++-- .../get-leads-by-agent.handler.ts | 41 ++++--- .../create-review/create-review.handler.ts | 110 +++++++++-------- .../delete-review/delete-review.handler.ts | 50 +++++--- .../get-average-rating.handler.ts | 16 ++- .../get-reviews-by-target.handler.ts | 26 ++-- .../get-reviews-by-user.handler.ts | 16 ++- 29 files changed, 897 insertions(+), 575 deletions(-) diff --git a/apps/api/src/modules/admin/application/commands/adjust-subscription/adjust-subscription.handler.ts b/apps/api/src/modules/admin/application/commands/adjust-subscription/adjust-subscription.handler.ts index 76276d1..f9db3a3 100644 --- a/apps/api/src/modules/admin/application/commands/adjust-subscription/adjust-subscription.handler.ts +++ b/apps/api/src/modules/admin/application/commands/adjust-subscription/adjust-subscription.handler.ts @@ -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 { type PlanTier } from '@prisma/client'; -import { NotFoundException, ValidationException, type PrismaService } from '@modules/shared'; +import { DomainException, NotFoundException, ValidationException, type PrismaService, type LoggerService } from '@modules/shared'; import { SUBSCRIPTION_REPOSITORY, type ISubscriptionRepository } from '@modules/subscriptions'; import { SubscriptionAdjustedEvent } from '../../../domain/events/subscription-adjusted.event'; import { AdjustSubscriptionCommand } from './adjust-subscription.command'; @@ -20,48 +20,59 @@ export class AdjustSubscriptionHandler implements ICommandHandler { - const tier = command.newPlanTier as PlanTier; - if (!VALID_TIERS.includes(tier)) { - throw new ValidationException(`Gói không hợp lệ: ${command.newPlanTier}`, { - validTiers: VALID_TIERS, + try { + const tier = command.newPlanTier as PlanTier; + if (!VALID_TIERS.includes(tier)) { + throw new ValidationException(`Gói không hợp lệ: ${command.newPlanTier}`, { + validTiers: VALID_TIERS, + }); + } + + const subscription = await this.subscriptionRepo.findByUserId(command.userId); + if (!subscription) { + throw new NotFoundException('Người dùng chưa có subscription'); + } + + const newPlan = await this.prisma.plan.findUnique({ + where: { tier }, }); + if (!newPlan) { + throw new NotFoundException(`Gói ${tier} không tồn tại trong hệ thống`); + } + + if (subscription.planId === newPlan.id) { + throw new ValidationException('Người dùng đã đang sử dụng gói này', { + currentPlanId: subscription.planId, + }); + } + + const upgradeResult = subscription.upgrade(newPlan.id, tier); + if (upgradeResult.isErr) { + throw upgradeResult.unwrapErr(); + } + await this.subscriptionRepo.update(subscription); + + this.eventBus.publish( + new SubscriptionAdjustedEvent(subscription.id, command.adminId, newPlan.id, command.reason), + ); + + return { + subscriptionId: subscription.id, + newPlanTier: tier, + message: `Subscription đã được chuyển sang gói ${tier}`, + }; + } catch (error) { + if (error instanceof DomainException) throw error; + this.logger.error( + `Failed to adjust subscription for user ${command.userId}: ${error instanceof Error ? error.message : String(error)}`, + error instanceof Error ? error.stack : undefined, + 'AdjustSubscriptionHandler', + ); + throw new InternalServerErrorException('Lỗi khi điều chỉnh subscription'); } - - const subscription = await this.subscriptionRepo.findByUserId(command.userId); - if (!subscription) { - throw new NotFoundException('Người dùng chưa có subscription'); - } - - const newPlan = await this.prisma.plan.findUnique({ - where: { tier }, - }); - if (!newPlan) { - throw new NotFoundException(`Gói ${tier} không tồn tại trong hệ thống`); - } - - if (subscription.planId === newPlan.id) { - throw new ValidationException('Người dùng đã đang sử dụng gói này', { - currentPlanId: subscription.planId, - }); - } - - const upgradeResult = subscription.upgrade(newPlan.id, tier); - if (upgradeResult.isErr) { - throw upgradeResult.unwrapErr(); - } - await this.subscriptionRepo.update(subscription); - - this.eventBus.publish( - new SubscriptionAdjustedEvent(subscription.id, command.adminId, newPlan.id, command.reason), - ); - - return { - subscriptionId: subscription.id, - newPlanTier: tier, - message: `Subscription đã được chuyển sang gói ${tier}`, - }; } } diff --git a/apps/api/src/modules/admin/application/commands/approve-kyc/approve-kyc.handler.ts b/apps/api/src/modules/admin/application/commands/approve-kyc/approve-kyc.handler.ts index b4ed5c7..038a534 100644 --- a/apps/api/src/modules/admin/application/commands/approve-kyc/approve-kyc.handler.ts +++ b/apps/api/src/modules/admin/application/commands/approve-kyc/approve-kyc.handler.ts @@ -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 { USER_REPOSITORY, type IUserRepository } from '@modules/auth'; -import { NotFoundException, ValidationException } from '@modules/shared'; +import { DomainException, NotFoundException, ValidationException, type LoggerService } from '@modules/shared'; import { KycApprovedEvent } from '../../../domain/events/kyc-approved.event'; import { ApproveKycCommand } from './approve-kyc.command'; @@ -16,30 +16,41 @@ export class ApproveKycHandler implements ICommandHandler { constructor( @Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository, private readonly eventBus: EventBus, + private readonly logger: LoggerService, ) {} async execute(command: ApproveKycCommand): Promise { - const user = await this.userRepo.findById(command.userId); - if (!user) { - throw new NotFoundException('Người dùng không tồn tại'); - } + try { + const user = await this.userRepo.findById(command.userId); + if (!user) { + throw new NotFoundException('Người dùng không tồn tại'); + } - if (user.kycStatus !== 'PENDING') { - throw new ValidationException( - `KYC đang ở trạng thái ${user.kycStatus}, chỉ có thể duyệt KYC đang chờ`, - { currentStatus: user.kycStatus }, + if (user.kycStatus !== 'PENDING') { + throw new ValidationException( + `KYC đang ở trạng thái ${user.kycStatus}, chỉ có thể duyệt KYC đang chờ`, + { currentStatus: user.kycStatus }, + ); + } + + user.updateKycStatus('VERIFIED'); + await this.userRepo.update(user); + + this.eventBus.publish(new KycApprovedEvent(user.id, command.adminId, command.comments)); + + return { + userId: user.id, + kycStatus: 'VERIFIED', + message: 'KYC đã được duyệt thành công', + }; + } catch (error) { + if (error instanceof DomainException) throw error; + this.logger.error( + `Failed to approve KYC for user ${command.userId}: ${error instanceof Error ? error.message : String(error)}`, + error instanceof Error ? error.stack : undefined, + 'ApproveKycHandler', ); + throw new InternalServerErrorException('Lỗi khi duyệt KYC'); } - - user.updateKycStatus('VERIFIED'); - await this.userRepo.update(user); - - this.eventBus.publish(new KycApprovedEvent(user.id, command.adminId, command.comments)); - - return { - userId: user.id, - kycStatus: 'VERIFIED', - message: 'KYC đã được duyệt thành công', - }; } } diff --git a/apps/api/src/modules/admin/application/commands/approve-listing/approve-listing.handler.ts b/apps/api/src/modules/admin/application/commands/approve-listing/approve-listing.handler.ts index ac1096d..7fc2ec3 100644 --- a/apps/api/src/modules/admin/application/commands/approve-listing/approve-listing.handler.ts +++ b/apps/api/src/modules/admin/application/commands/approve-listing/approve-listing.handler.ts @@ -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 { LISTING_REPOSITORY, type IListingRepository } from '@modules/listings'; -import { NotFoundException, ValidationException } from '@modules/shared'; +import { DomainException, NotFoundException, ValidationException, type LoggerService } from '@modules/shared'; import { ListingApprovedEvent } from '../../../domain/events/listing-approved.event'; import { ApproveListingCommand } from './approve-listing.command'; @@ -16,37 +16,48 @@ export class ApproveListingHandler implements ICommandHandler { - const listing = await this.listingRepo.findById(command.listingId); - if (!listing) { - throw new NotFoundException('Listing không tồn tại'); - } + try { + const listing = await this.listingRepo.findById(command.listingId); + if (!listing) { + throw new NotFoundException('Listing không tồn tại'); + } - if (listing.status !== 'PENDING_REVIEW') { - throw new ValidationException( - `Listing đang ở trạng thái ${listing.status}, chỉ có thể duyệt listing đang chờ duyệt`, - { currentStatus: listing.status }, + if (listing.status !== 'PENDING_REVIEW') { + throw new ValidationException( + `Listing đang ở trạng thái ${listing.status}, chỉ có thể duyệt listing đang chờ duyệt`, + { currentStatus: listing.status }, + ); + } + + listing.approve(); + + if (command.moderationNotes) { + listing.setModerationScore(1.0, command.moderationNotes); + } + + await this.listingRepo.update(listing); + + this.eventBus.publish( + new ListingApprovedEvent(listing.id, command.adminId, command.moderationNotes), ); + + return { + listingId: listing.id, + status: 'ACTIVE', + message: 'Listing đã được duyệt thành công', + }; + } catch (error) { + if (error instanceof DomainException) throw error; + this.logger.error( + `Failed to approve listing ${command.listingId}: ${error instanceof Error ? error.message : String(error)}`, + error instanceof Error ? error.stack : undefined, + 'ApproveListingHandler', + ); + throw new InternalServerErrorException('Lỗi khi duyệt listing'); } - - listing.approve(); - - if (command.moderationNotes) { - listing.setModerationScore(1.0, command.moderationNotes); - } - - await this.listingRepo.update(listing); - - this.eventBus.publish( - new ListingApprovedEvent(listing.id, command.adminId, command.moderationNotes), - ); - - return { - listingId: listing.id, - status: 'ACTIVE', - message: 'Listing đã được duyệt thành công', - }; } } diff --git a/apps/api/src/modules/admin/application/commands/ban-user/ban-user.handler.ts b/apps/api/src/modules/admin/application/commands/ban-user/ban-user.handler.ts index 0f75010..765a302 100644 --- a/apps/api/src/modules/admin/application/commands/ban-user/ban-user.handler.ts +++ b/apps/api/src/modules/admin/application/commands/ban-user/ban-user.handler.ts @@ -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 { USER_REPOSITORY, type IUserRepository } from '@modules/auth'; -import { NotFoundException, ValidationException } from '@modules/shared'; +import { DomainException, NotFoundException, ValidationException, type LoggerService } from '@modules/shared'; import { UserBannedEvent } from '../../../domain/events/user-banned.event'; import { UserUnbannedEvent } from '../../../domain/events/user-unbanned.event'; import { BanUserCommand } from './ban-user.command'; @@ -17,54 +17,65 @@ export class BanUserHandler implements ICommandHandler { constructor( @Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository, private readonly eventBus: EventBus, + private readonly logger: LoggerService, ) {} async execute(command: BanUserCommand): Promise { - const user = await this.userRepo.findById(command.userId); - if (!user) { - throw new NotFoundException('Người dùng không tồn tại'); - } + try { + const user = await this.userRepo.findById(command.userId); + if (!user) { + throw new NotFoundException('Người dùng không tồn tại'); + } - if (user.role === 'ADMIN') { - throw new ValidationException('Không thể ban/unban tài khoản admin', { - userId: command.userId, - }); - } - - if (command.unban) { - if (user.isActive) { - throw new ValidationException('Người dùng chưa bị ban', { + if (user.role === 'ADMIN') { + throw new ValidationException('Không thể ban/unban tài khoản admin', { userId: command.userId, }); } - user.activate(); + if (command.unban) { + if (user.isActive) { + throw new ValidationException('Người dùng chưa bị ban', { + userId: command.userId, + }); + } + + user.activate(); + await this.userRepo.update(user); + + this.eventBus.publish(new UserUnbannedEvent(user.id, command.adminId)); + + return { + userId: user.id, + isActive: true, + message: 'Người dùng đã được gỡ ban', + }; + } + + if (!user.isActive) { + throw new ValidationException('Người dùng đã bị ban', { + userId: command.userId, + }); + } + + user.deactivate(); await this.userRepo.update(user); - this.eventBus.publish(new UserUnbannedEvent(user.id, command.adminId)); + this.eventBus.publish(new UserBannedEvent(user.id, command.adminId, command.reason)); return { userId: user.id, - isActive: true, - message: 'Người dùng đã được gỡ ban', + isActive: false, + message: 'Người dùng đã bị ban', }; + } catch (error) { + if (error instanceof DomainException) throw error; + this.logger.error( + `Failed to ban/unban user ${command.userId}: ${error instanceof Error ? error.message : String(error)}`, + error instanceof Error ? error.stack : undefined, + 'BanUserHandler', + ); + throw new InternalServerErrorException('Lỗi khi ban/unban người dùng'); } - - if (!user.isActive) { - throw new ValidationException('Người dùng đã bị ban', { - userId: command.userId, - }); - } - - user.deactivate(); - await this.userRepo.update(user); - - this.eventBus.publish(new UserBannedEvent(user.id, command.adminId, command.reason)); - - return { - userId: user.id, - isActive: false, - message: 'Người dùng đã bị ban', - }; } } diff --git a/apps/api/src/modules/admin/application/commands/bulk-moderate-listings/bulk-moderate-listings.handler.ts b/apps/api/src/modules/admin/application/commands/bulk-moderate-listings/bulk-moderate-listings.handler.ts index dc72cca..4cf248c 100644 --- a/apps/api/src/modules/admin/application/commands/bulk-moderate-listings/bulk-moderate-listings.handler.ts +++ b/apps/api/src/modules/admin/application/commands/bulk-moderate-listings/bulk-moderate-listings.handler.ts @@ -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 { LISTING_REPOSITORY, type IListingRepository } from '@modules/listings'; -import { ValidationException } from '@modules/shared'; +import { DomainException, ValidationException, type LoggerService } from '@modules/shared'; import { ListingApprovedEvent } from '../../../domain/events/listing-approved.event'; import { ListingRejectedEvent } from '../../../domain/events/listing-rejected.event'; import { BulkModerateListingsCommand } from './bulk-moderate-listings.command'; @@ -17,60 +17,71 @@ export class BulkModerateListingsHandler implements ICommandHandler { - if (command.listingIds.length === 0) { - throw new ValidationException('Danh sách listing không được rỗng', {}); - } - - if (command.listingIds.length > 50) { - throw new ValidationException('Tối đa 50 listing mỗi lần', { - count: command.listingIds.length, - }); - } - - if (command.action === 'reject' && !command.reason) { - throw new ValidationException('Lý do từ chối là bắt buộc', {}); - } - - const succeeded: string[] = []; - const failed: Array<{ listingId: string; reason: string }> = []; - - for (const listingId of command.listingIds) { - try { - const listing = await this.listingRepo.findById(listingId); - if (!listing) { - failed.push({ listingId, reason: 'Listing không tồn tại' }); - continue; - } - - if (listing.status !== 'PENDING_REVIEW') { - failed.push({ listingId, reason: `Trạng thái hiện tại: ${listing.status}` }); - continue; - } - - if (command.action === 'approve') { - listing.approve(); - await this.listingRepo.update(listing); - this.eventBus.publish(new ListingApprovedEvent(listing.id, command.adminId)); - } else { - listing.reject(command.reason!); - await this.listingRepo.update(listing); - this.eventBus.publish(new ListingRejectedEvent(listing.id, command.adminId, command.reason!)); - } - - succeeded.push(listingId); - } catch (error) { - const message = error instanceof Error ? error.message : 'Lỗi không xác định'; - failed.push({ listingId, reason: message }); + try { + if (command.listingIds.length === 0) { + throw new ValidationException('Danh sách listing không được rỗng', {}); } - } - return { - processed: command.listingIds.length, - succeeded, - failed, - }; + if (command.listingIds.length > 50) { + throw new ValidationException('Tối đa 50 listing mỗi lần', { + count: command.listingIds.length, + }); + } + + if (command.action === 'reject' && !command.reason) { + throw new ValidationException('Lý do từ chối là bắt buộc', {}); + } + + const succeeded: string[] = []; + const failed: Array<{ listingId: string; reason: string }> = []; + + for (const listingId of command.listingIds) { + try { + const listing = await this.listingRepo.findById(listingId); + if (!listing) { + failed.push({ listingId, reason: 'Listing không tồn tại' }); + continue; + } + + if (listing.status !== 'PENDING_REVIEW') { + failed.push({ listingId, reason: `Trạng thái hiện tại: ${listing.status}` }); + continue; + } + + if (command.action === 'approve') { + listing.approve(); + await this.listingRepo.update(listing); + this.eventBus.publish(new ListingApprovedEvent(listing.id, command.adminId)); + } else { + listing.reject(command.reason!); + await this.listingRepo.update(listing); + this.eventBus.publish(new ListingRejectedEvent(listing.id, command.adminId, command.reason!)); + } + + succeeded.push(listingId); + } catch (error) { + const message = error instanceof Error ? error.message : 'Lỗi không xác định'; + failed.push({ listingId, reason: message }); + } + } + + return { + processed: command.listingIds.length, + succeeded, + failed, + }; + } catch (error) { + if (error instanceof DomainException) throw error; + this.logger.error( + `Failed to bulk moderate listings: ${error instanceof Error ? error.message : String(error)}`, + error instanceof Error ? error.stack : undefined, + 'BulkModerateListingsHandler', + ); + throw new InternalServerErrorException('Lỗi khi duyệt/từ chối hàng loạt listing'); + } } } diff --git a/apps/api/src/modules/admin/application/commands/reject-kyc/reject-kyc.handler.ts b/apps/api/src/modules/admin/application/commands/reject-kyc/reject-kyc.handler.ts index b402944..3e6892a 100644 --- a/apps/api/src/modules/admin/application/commands/reject-kyc/reject-kyc.handler.ts +++ b/apps/api/src/modules/admin/application/commands/reject-kyc/reject-kyc.handler.ts @@ -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 { USER_REPOSITORY, type IUserRepository } from '@modules/auth'; -import { NotFoundException, ValidationException } from '@modules/shared'; +import { DomainException, NotFoundException, ValidationException, type LoggerService } from '@modules/shared'; import { KycRejectedEvent } from '../../../domain/events/kyc-rejected.event'; import { RejectKycCommand } from './reject-kyc.command'; @@ -16,30 +16,41 @@ export class RejectKycHandler implements ICommandHandler { constructor( @Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository, private readonly eventBus: EventBus, + private readonly logger: LoggerService, ) {} async execute(command: RejectKycCommand): Promise { - const user = await this.userRepo.findById(command.userId); - if (!user) { - throw new NotFoundException('Người dùng không tồn tại'); - } + try { + const user = await this.userRepo.findById(command.userId); + if (!user) { + throw new NotFoundException('Người dùng không tồn tại'); + } - if (user.kycStatus !== 'PENDING') { - throw new ValidationException( - `KYC đang ở trạng thái ${user.kycStatus}, chỉ có thể từ chối KYC đang chờ`, - { currentStatus: user.kycStatus }, + if (user.kycStatus !== 'PENDING') { + throw new ValidationException( + `KYC đang ở trạng thái ${user.kycStatus}, chỉ có thể từ chối KYC đang chờ`, + { currentStatus: user.kycStatus }, + ); + } + + user.updateKycStatus('REJECTED'); + await this.userRepo.update(user); + + this.eventBus.publish(new KycRejectedEvent(user.id, command.adminId, command.reason)); + + return { + userId: user.id, + kycStatus: 'REJECTED', + message: 'KYC đã bị từ chối', + }; + } catch (error) { + if (error instanceof DomainException) throw error; + this.logger.error( + `Failed to reject KYC for user ${command.userId}: ${error instanceof Error ? error.message : String(error)}`, + error instanceof Error ? error.stack : undefined, + 'RejectKycHandler', ); + throw new InternalServerErrorException('Lỗi khi từ chối KYC'); } - - user.updateKycStatus('REJECTED'); - await this.userRepo.update(user); - - this.eventBus.publish(new KycRejectedEvent(user.id, command.adminId, command.reason)); - - return { - userId: user.id, - kycStatus: 'REJECTED', - message: 'KYC đã bị từ chối', - }; } } diff --git a/apps/api/src/modules/admin/application/commands/reject-listing/reject-listing.handler.ts b/apps/api/src/modules/admin/application/commands/reject-listing/reject-listing.handler.ts index eb706bd..825b63d 100644 --- a/apps/api/src/modules/admin/application/commands/reject-listing/reject-listing.handler.ts +++ b/apps/api/src/modules/admin/application/commands/reject-listing/reject-listing.handler.ts @@ -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 { LISTING_REPOSITORY, type IListingRepository } from '@modules/listings'; -import { NotFoundException, ValidationException } from '@modules/shared'; +import { DomainException, NotFoundException, ValidationException, type LoggerService } from '@modules/shared'; import { ListingRejectedEvent } from '../../../domain/events/listing-rejected.event'; import { RejectListingCommand } from './reject-listing.command'; @@ -16,32 +16,43 @@ export class RejectListingHandler implements ICommandHandler { - const listing = await this.listingRepo.findById(command.listingId); - if (!listing) { - throw new NotFoundException('Listing không tồn tại'); - } + try { + const listing = await this.listingRepo.findById(command.listingId); + if (!listing) { + throw new NotFoundException('Listing không tồn tại'); + } - if (listing.status !== 'PENDING_REVIEW') { - throw new ValidationException( - `Listing đang ở trạng thái ${listing.status}, chỉ có thể từ chối listing đang chờ duyệt`, - { currentStatus: listing.status }, + if (listing.status !== 'PENDING_REVIEW') { + throw new ValidationException( + `Listing đang ở trạng thái ${listing.status}, chỉ có thể từ chối listing đang chờ duyệt`, + { currentStatus: listing.status }, + ); + } + + listing.reject(command.reason); + await this.listingRepo.update(listing); + + this.eventBus.publish( + new ListingRejectedEvent(listing.id, command.adminId, command.reason), ); + + return { + listingId: listing.id, + status: 'REJECTED', + message: 'Listing đã bị từ chối', + }; + } catch (error) { + if (error instanceof DomainException) throw error; + this.logger.error( + `Failed to reject listing ${command.listingId}: ${error instanceof Error ? error.message : String(error)}`, + error instanceof Error ? error.stack : undefined, + 'RejectListingHandler', + ); + throw new InternalServerErrorException('Lỗi khi từ chối listing'); } - - listing.reject(command.reason); - await this.listingRepo.update(listing); - - this.eventBus.publish( - new ListingRejectedEvent(listing.id, command.adminId, command.reason), - ); - - return { - listingId: listing.id, - status: 'REJECTED', - message: 'Listing đã bị từ chối', - }; } } diff --git a/apps/api/src/modules/admin/application/commands/update-user-status/update-user-status.handler.ts b/apps/api/src/modules/admin/application/commands/update-user-status/update-user-status.handler.ts index 0dd02fb..af3a088 100644 --- a/apps/api/src/modules/admin/application/commands/update-user-status/update-user-status.handler.ts +++ b/apps/api/src/modules/admin/application/commands/update-user-status/update-user-status.handler.ts @@ -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 { USER_REPOSITORY, type IUserRepository } from '@modules/auth'; -import { NotFoundException, ValidationException } from '@modules/shared'; +import { DomainException, NotFoundException, ValidationException, type LoggerService } from '@modules/shared'; import { UserBannedEvent } from '../../../domain/events/user-banned.event'; import { UserUnbannedEvent } from '../../../domain/events/user-unbanned.event'; import { UpdateUserStatusCommand } from './update-user-status.command'; @@ -17,47 +17,58 @@ export class UpdateUserStatusHandler implements ICommandHandler { - const user = await this.userRepo.findById(command.userId); - if (!user) { - throw new NotFoundException('Người dùng không tồn tại'); - } + try { + const user = await this.userRepo.findById(command.userId); + if (!user) { + throw new NotFoundException('Người dùng không tồn tại'); + } - if (user.role === 'ADMIN') { - throw new ValidationException('Không thể thay đổi trạng thái tài khoản admin', { - userId: command.userId, - }); - } + if (user.role === 'ADMIN') { + throw new ValidationException('Không thể thay đổi trạng thái tài khoản admin', { + userId: command.userId, + }); + } - if (user.isActive === command.isActive) { - const statusLabel = command.isActive ? 'đang hoạt động' : 'đã bị vô hiệu hóa'; - throw new ValidationException(`Người dùng ${statusLabel}`, { - userId: command.userId, - }); - } + if (user.isActive === command.isActive) { + const statusLabel = command.isActive ? 'đang hoạt động' : 'đã bị vô hiệu hóa'; + throw new ValidationException(`Người dùng ${statusLabel}`, { + userId: command.userId, + }); + } - if (command.isActive) { - user.activate(); + if (command.isActive) { + user.activate(); + await this.userRepo.update(user); + this.eventBus.publish(new UserUnbannedEvent(user.id, command.adminId)); + + return { + userId: user.id, + isActive: true, + message: 'Người dùng đã được kích hoạt', + }; + } + + user.deactivate(); await this.userRepo.update(user); - this.eventBus.publish(new UserUnbannedEvent(user.id, command.adminId)); + this.eventBus.publish(new UserBannedEvent(user.id, command.adminId, command.reason)); return { userId: user.id, - isActive: true, - message: 'Người dùng đã được kích hoạt', + isActive: false, + message: 'Người dùng đã bị vô hiệu hóa', }; + } catch (error) { + if (error instanceof DomainException) throw error; + this.logger.error( + `Failed to update user status for ${command.userId}: ${error instanceof Error ? error.message : String(error)}`, + error instanceof Error ? error.stack : undefined, + 'UpdateUserStatusHandler', + ); + throw new InternalServerErrorException('Lỗi khi cập nhật trạng thái người dùng'); } - - user.deactivate(); - await this.userRepo.update(user); - this.eventBus.publish(new UserBannedEvent(user.id, command.adminId, command.reason)); - - return { - userId: user.id, - isActive: false, - message: 'Người dùng đã bị vô hiệu hóa', - }; } } diff --git a/apps/api/src/modules/admin/application/queries/get-audit-logs/get-audit-logs.handler.ts b/apps/api/src/modules/admin/application/queries/get-audit-logs/get-audit-logs.handler.ts index 6993dcf..7d9eb06 100644 --- a/apps/api/src/modules/admin/application/queries/get-audit-logs/get-audit-logs.handler.ts +++ b/apps/api/src/modules/admin/application/queries/get-audit-logs/get-audit-logs.handler.ts @@ -1,5 +1,6 @@ -import { Inject } from '@nestjs/common'; +import { Inject, InternalServerErrorException } from '@nestjs/common'; import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs'; +import { DomainException, type LoggerService } from '@modules/shared'; import { AUDIT_LOG_REPOSITORY, type IAuditLogRepository, @@ -11,18 +12,29 @@ import { GetAuditLogsQuery } from './get-audit-logs.query'; export class GetAuditLogsHandler implements IQueryHandler { constructor( @Inject(AUDIT_LOG_REPOSITORY) private readonly auditRepo: IAuditLogRepository, + private readonly logger: LoggerService, ) {} async execute(query: GetAuditLogsQuery): Promise { - return this.auditRepo.findAll({ - page: query.page, - limit: query.limit, - action: query.action, - actorId: query.actorId, - targetId: query.targetId, - targetType: query.targetType, - startDate: query.startDate, - endDate: query.endDate, - }); + try { + return await this.auditRepo.findAll({ + page: query.page, + limit: query.limit, + action: query.action, + actorId: query.actorId, + targetId: query.targetId, + targetType: query.targetType, + startDate: query.startDate, + endDate: query.endDate, + }); + } catch (error) { + if (error instanceof DomainException) throw error; + this.logger.error( + `Failed to get audit logs: ${error instanceof Error ? error.message : String(error)}`, + error instanceof Error ? error.stack : undefined, + 'GetAuditLogsHandler', + ); + throw new InternalServerErrorException('Lỗi khi lấy nhật ký kiểm toán'); + } } } diff --git a/apps/api/src/modules/admin/application/queries/get-dashboard-stats/get-dashboard-stats.handler.ts b/apps/api/src/modules/admin/application/queries/get-dashboard-stats/get-dashboard-stats.handler.ts index 451e9ad..472a163 100644 --- a/apps/api/src/modules/admin/application/queries/get-dashboard-stats/get-dashboard-stats.handler.ts +++ b/apps/api/src/modules/admin/application/queries/get-dashboard-stats/get-dashboard-stats.handler.ts @@ -1,5 +1,6 @@ -import { Inject } from '@nestjs/common'; +import { Inject, InternalServerErrorException } from '@nestjs/common'; import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs'; +import { DomainException, type LoggerService } from '@modules/shared'; import { ADMIN_QUERY_REPOSITORY, type IAdminQueryRepository, type DashboardStats } from '../../../domain/repositories/admin-query.repository'; import { GetDashboardStatsQuery } from './get-dashboard-stats.query'; @@ -7,9 +8,20 @@ import { GetDashboardStatsQuery } from './get-dashboard-stats.query'; export class GetDashboardStatsHandler implements IQueryHandler { constructor( @Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository, + private readonly logger: LoggerService, ) {} async execute(_query: GetDashboardStatsQuery): Promise { - return this.adminQueryRepo.getDashboardStats(); + try { + return await this.adminQueryRepo.getDashboardStats(); + } catch (error) { + if (error instanceof DomainException) throw error; + this.logger.error( + `Failed to get dashboard stats: ${error instanceof Error ? error.message : String(error)}`, + error instanceof Error ? error.stack : undefined, + 'GetDashboardStatsHandler', + ); + throw new InternalServerErrorException('Lỗi khi lấy thống kê dashboard'); + } } } diff --git a/apps/api/src/modules/admin/application/queries/get-kyc-queue/get-kyc-queue.handler.ts b/apps/api/src/modules/admin/application/queries/get-kyc-queue/get-kyc-queue.handler.ts index 98e6814..96f8c46 100644 --- a/apps/api/src/modules/admin/application/queries/get-kyc-queue/get-kyc-queue.handler.ts +++ b/apps/api/src/modules/admin/application/queries/get-kyc-queue/get-kyc-queue.handler.ts @@ -1,5 +1,6 @@ -import { Inject } from '@nestjs/common'; +import { Inject, InternalServerErrorException } from '@nestjs/common'; import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs'; +import { DomainException, type LoggerService } from '@modules/shared'; import { ADMIN_QUERY_REPOSITORY, type IAdminQueryRepository, type KycQueueResult } from '../../../domain/repositories/admin-query.repository'; import { GetKycQueueQuery } from './get-kyc-queue.query'; @@ -7,9 +8,20 @@ import { GetKycQueueQuery } from './get-kyc-queue.query'; export class GetKycQueueHandler implements IQueryHandler { constructor( @Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository, + private readonly logger: LoggerService, ) {} async execute(query: GetKycQueueQuery): Promise { - return this.adminQueryRepo.getKycQueue(query.page, query.limit); + try { + return await this.adminQueryRepo.getKycQueue(query.page, query.limit); + } catch (error) { + if (error instanceof DomainException) throw error; + this.logger.error( + `Failed to get KYC queue: ${error instanceof Error ? error.message : String(error)}`, + error instanceof Error ? error.stack : undefined, + 'GetKycQueueHandler', + ); + throw new InternalServerErrorException('Lỗi khi lấy danh sách KYC'); + } } } diff --git a/apps/api/src/modules/admin/application/queries/get-moderation-queue/get-moderation-queue.handler.ts b/apps/api/src/modules/admin/application/queries/get-moderation-queue/get-moderation-queue.handler.ts index c6471a1..5b4b6da 100644 --- a/apps/api/src/modules/admin/application/queries/get-moderation-queue/get-moderation-queue.handler.ts +++ b/apps/api/src/modules/admin/application/queries/get-moderation-queue/get-moderation-queue.handler.ts @@ -1,5 +1,6 @@ -import { Inject } from '@nestjs/common'; +import { Inject, InternalServerErrorException } from '@nestjs/common'; import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs'; +import { DomainException, type LoggerService } from '@modules/shared'; import { ADMIN_QUERY_REPOSITORY, type IAdminQueryRepository, type ModerationQueueResult } from '../../../domain/repositories/admin-query.repository'; import { GetModerationQueueQuery } from './get-moderation-queue.query'; @@ -7,9 +8,20 @@ import { GetModerationQueueQuery } from './get-moderation-queue.query'; export class GetModerationQueueHandler implements IQueryHandler { constructor( @Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository, + private readonly logger: LoggerService, ) {} async execute(query: GetModerationQueueQuery): Promise { - return this.adminQueryRepo.getModerationQueue(query.page, query.limit); + try { + return await this.adminQueryRepo.getModerationQueue(query.page, query.limit); + } catch (error) { + if (error instanceof DomainException) throw error; + this.logger.error( + `Failed to get moderation queue: ${error instanceof Error ? error.message : String(error)}`, + error instanceof Error ? error.stack : undefined, + 'GetModerationQueueHandler', + ); + throw new InternalServerErrorException('Lỗi khi lấy danh sách duyệt'); + } } } diff --git a/apps/api/src/modules/admin/application/queries/get-revenue-stats/get-revenue-stats.handler.ts b/apps/api/src/modules/admin/application/queries/get-revenue-stats/get-revenue-stats.handler.ts index 9923425..33329fa 100644 --- a/apps/api/src/modules/admin/application/queries/get-revenue-stats/get-revenue-stats.handler.ts +++ b/apps/api/src/modules/admin/application/queries/get-revenue-stats/get-revenue-stats.handler.ts @@ -1,5 +1,6 @@ -import { Inject } from '@nestjs/common'; +import { Inject, InternalServerErrorException } from '@nestjs/common'; import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs'; +import { DomainException, type LoggerService } from '@modules/shared'; import { ADMIN_QUERY_REPOSITORY, type IAdminQueryRepository, type RevenueStatsItem } from '../../../domain/repositories/admin-query.repository'; import { GetRevenueStatsQuery } from './get-revenue-stats.query'; @@ -7,9 +8,20 @@ import { GetRevenueStatsQuery } from './get-revenue-stats.query'; export class GetRevenueStatsHandler implements IQueryHandler { constructor( @Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository, + private readonly logger: LoggerService, ) {} async execute(query: GetRevenueStatsQuery): Promise { - return this.adminQueryRepo.getRevenueStats(query.startDate, query.endDate, query.groupBy); + try { + return await this.adminQueryRepo.getRevenueStats(query.startDate, query.endDate, query.groupBy); + } catch (error) { + if (error instanceof DomainException) throw error; + this.logger.error( + `Failed to get revenue stats: ${error instanceof Error ? error.message : String(error)}`, + error instanceof Error ? error.stack : undefined, + 'GetRevenueStatsHandler', + ); + throw new InternalServerErrorException('Lỗi khi lấy thống kê doanh thu'); + } } } diff --git a/apps/api/src/modules/admin/application/queries/get-user-detail/get-user-detail.handler.ts b/apps/api/src/modules/admin/application/queries/get-user-detail/get-user-detail.handler.ts index b562c9f..3dadb78 100644 --- a/apps/api/src/modules/admin/application/queries/get-user-detail/get-user-detail.handler.ts +++ b/apps/api/src/modules/admin/application/queries/get-user-detail/get-user-detail.handler.ts @@ -1,6 +1,6 @@ -import { Inject } from '@nestjs/common'; +import { Inject, InternalServerErrorException } from '@nestjs/common'; import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs'; -import { NotFoundException } from '@modules/shared'; +import { DomainException, NotFoundException, type LoggerService } from '@modules/shared'; import { ADMIN_QUERY_REPOSITORY, type IAdminQueryRepository, type UserDetail } from '../../../domain/repositories/admin-query.repository'; import { GetUserDetailQuery } from './get-user-detail.query'; @@ -8,13 +8,24 @@ import { GetUserDetailQuery } from './get-user-detail.query'; export class GetUserDetailHandler implements IQueryHandler { constructor( @Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository, + private readonly logger: LoggerService, ) {} async execute(query: GetUserDetailQuery): Promise { - const user = await this.adminQueryRepo.getUserDetail(query.userId); - if (!user) { - throw new NotFoundException('Người dùng không tồn tại'); + try { + const user = await this.adminQueryRepo.getUserDetail(query.userId); + if (!user) { + throw new NotFoundException('Người dùng không tồn tại'); + } + return user; + } catch (error) { + if (error instanceof DomainException) throw error; + this.logger.error( + `Failed to get user detail for ${query.userId}: ${error instanceof Error ? error.message : String(error)}`, + error instanceof Error ? error.stack : undefined, + 'GetUserDetailHandler', + ); + throw new InternalServerErrorException('Lỗi khi lấy thông tin người dùng'); } - return user; } } diff --git a/apps/api/src/modules/admin/application/queries/get-users/get-users.handler.ts b/apps/api/src/modules/admin/application/queries/get-users/get-users.handler.ts index c9ad02b..e6b9fa9 100644 --- a/apps/api/src/modules/admin/application/queries/get-users/get-users.handler.ts +++ b/apps/api/src/modules/admin/application/queries/get-users/get-users.handler.ts @@ -1,5 +1,6 @@ -import { Inject } from '@nestjs/common'; +import { Inject, InternalServerErrorException } from '@nestjs/common'; import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs'; +import { DomainException, type LoggerService } from '@modules/shared'; import { ADMIN_QUERY_REPOSITORY, type IAdminQueryRepository, type UserListResult } from '../../../domain/repositories/admin-query.repository'; import { GetUsersQuery } from './get-users.query'; @@ -7,15 +8,26 @@ import { GetUsersQuery } from './get-users.query'; export class GetUsersHandler implements IQueryHandler { constructor( @Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository, + private readonly logger: LoggerService, ) {} async execute(query: GetUsersQuery): Promise { - return this.adminQueryRepo.getUsers({ - page: query.page, - limit: query.limit, - role: query.role, - isActive: query.isActive, - search: query.search, - }); + try { + return await this.adminQueryRepo.getUsers({ + page: query.page, + limit: query.limit, + role: query.role, + isActive: query.isActive, + search: query.search, + }); + } catch (error) { + if (error instanceof DomainException) throw error; + this.logger.error( + `Failed to get users: ${error instanceof Error ? error.message : String(error)}`, + error instanceof Error ? error.stack : undefined, + 'GetUsersHandler', + ); + throw new InternalServerErrorException('Lỗi khi lấy danh sách người dùng'); + } } } diff --git a/apps/api/src/modules/inquiries/application/commands/create-inquiry/create-inquiry.handler.ts b/apps/api/src/modules/inquiries/application/commands/create-inquiry/create-inquiry.handler.ts index 605a7b1..19f4275 100644 --- a/apps/api/src/modules/inquiries/application/commands/create-inquiry/create-inquiry.handler.ts +++ b/apps/api/src/modules/inquiries/application/commands/create-inquiry/create-inquiry.handler.ts @@ -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 { - // 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(), - }; } } diff --git a/apps/api/src/modules/inquiries/application/commands/mark-inquiry-read/mark-inquiry-read.handler.ts b/apps/api/src/modules/inquiries/application/commands/mark-inquiry-read/mark-inquiry-read.handler.ts index 5687840..90c08e2 100644 --- a/apps/api/src/modules/inquiries/application/commands/mark-inquiry-read/mark-inquiry-read.handler.ts +++ b/apps/api/src/modules/inquiries/application/commands/mark-inquiry-read/mark-inquiry-read.handler.ts @@ -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 { - 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'); } } diff --git a/apps/api/src/modules/inquiries/application/queries/get-inquiries-by-agent/get-inquiries-by-agent.handler.ts b/apps/api/src/modules/inquiries/application/queries/get-inquiries-by-agent/get-inquiries-by-agent.handler.ts index 1edf7e6..a7fa962 100644 --- a/apps/api/src/modules/inquiries/application/queries/get-inquiries-by-agent/get-inquiries-by-agent.handler.ts +++ b/apps/api/src/modules/inquiries/application/queries/get-inquiries-by-agent/get-inquiries-by-agent.handler.ts @@ -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> { - 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'); + } } } diff --git a/apps/api/src/modules/inquiries/application/queries/get-inquiries-by-listing/get-inquiries-by-listing.handler.ts b/apps/api/src/modules/inquiries/application/queries/get-inquiries-by-listing/get-inquiries-by-listing.handler.ts index 4d610c1..499e939 100644 --- a/apps/api/src/modules/inquiries/application/queries/get-inquiries-by-listing/get-inquiries-by-listing.handler.ts +++ b/apps/api/src/modules/inquiries/application/queries/get-inquiries-by-listing/get-inquiries-by-listing.handler.ts @@ -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 { constructor( @Inject(INQUIRY_REPOSITORY) private readonly inquiryRepo: IInquiryRepository, + private readonly logger: LoggerService, ) {} async execute(query: GetInquiriesByListingQuery): Promise> { - 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'); + } } } diff --git a/apps/api/src/modules/leads/application/commands/create-lead/create-lead.handler.ts b/apps/api/src/modules/leads/application/commands/create-lead/create-lead.handler.ts index 65d6178..89d7abd 100644 --- a/apps/api/src/modules/leads/application/commands/create-lead/create-lead.handler.ts +++ b/apps/api/src/modules/leads/application/commands/create-lead/create-lead.handler.ts @@ -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 { ) {} async execute(command: CreateLeadCommand): Promise { - // 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(), - }; } } diff --git a/apps/api/src/modules/leads/application/commands/delete-lead/delete-lead.handler.ts b/apps/api/src/modules/leads/application/commands/delete-lead/delete-lead.handler.ts index f02d42a..3191341 100644 --- a/apps/api/src/modules/leads/application/commands/delete-lead/delete-lead.handler.ts +++ b/apps/api/src/modules/leads/application/commands/delete-lead/delete-lead.handler.ts @@ -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 { ) {} async execute(command: DeleteLeadCommand): Promise { - // 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'); } } diff --git a/apps/api/src/modules/leads/application/commands/update-lead-status/update-lead-status.handler.ts b/apps/api/src/modules/leads/application/commands/update-lead-status/update-lead-status.handler.ts index 2edb31c..250d05b 100644 --- a/apps/api/src/modules/leads/application/commands/update-lead-status/update-lead-status.handler.ts +++ b/apps/api/src/modules/leads/application/commands/update-lead-status/update-lead-status.handler.ts @@ -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 { - // 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'); } } diff --git a/apps/api/src/modules/leads/application/queries/get-lead-stats/get-lead-stats.handler.ts b/apps/api/src/modules/leads/application/queries/get-lead-stats/get-lead-stats.handler.ts index 0ff6b49..29dddcc 100644 --- a/apps/api/src/modules/leads/application/queries/get-lead-stats/get-lead-stats.handler.ts +++ b/apps/api/src/modules/leads/application/queries/get-lead-stats/get-lead-stats.handler.ts @@ -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 { constructor( @Inject(LEAD_REPOSITORY) private readonly leadRepo: ILeadRepository, private readonly prisma: PrismaService, + private readonly logger: LoggerService, ) {} async execute(query: GetLeadStatsQuery): Promise { - // 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'); + } } } diff --git a/apps/api/src/modules/leads/application/queries/get-leads-by-agent/get-leads-by-agent.handler.ts b/apps/api/src/modules/leads/application/queries/get-leads-by-agent/get-leads-by-agent.handler.ts index 7319b69..36fdccc 100644 --- a/apps/api/src/modules/leads/application/queries/get-leads-by-agent/get-leads-by-agent.handler.ts +++ b/apps/api/src/modules/leads/application/queries/get-leads-by-agent/get-leads-by-agent.handler.ts @@ -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> { - // 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'); + } } } diff --git a/apps/api/src/modules/reviews/application/commands/create-review/create-review.handler.ts b/apps/api/src/modules/reviews/application/commands/create-review/create-review.handler.ts index 35b996c..7eae66d 100644 --- a/apps/api/src/modules/reviews/application/commands/create-review/create-review.handler.ts +++ b/apps/api/src/modules/reviews/application/commands/create-review/create-review.handler.ts @@ -1,9 +1,9 @@ -import { Inject } from '@nestjs/common'; +import { Inject, InternalServerErrorException } from '@nestjs/common'; // eslint-disable-next-line @typescript-eslint/consistent-type-imports -- NestJS DI requires value imports for emitDecoratorMetadata import { CommandHandler, EventBus, ICommandHandler } from '@nestjs/cqrs'; import { createId } from '@paralleldrive/cuid2'; // eslint-disable-next-line @typescript-eslint/consistent-type-imports -- NestJS DI requires value imports for emitDecoratorMetadata -import { ConflictException, ValidationException, LoggerService } from '@modules/shared'; +import { ConflictException, DomainException, ValidationException, LoggerService } from '@modules/shared'; import { ReviewEntity } from '../../../domain/entities/review.entity'; import { REVIEW_REPOSITORY, type IReviewRepository } from '../../../domain/repositories/review.repository'; import { Rating } from '../../../domain/value-objects/rating.vo'; @@ -26,54 +26,64 @@ export class CreateReviewHandler implements ICommandHandler ) {} async execute(command: CreateReviewCommand): Promise { - // Validate rating value object - const ratingResult = Rating.create(command.rating); - if (ratingResult.isErr) { - throw new ValidationException(ratingResult.unwrapErr()); + try { + // Validate rating value object + const ratingResult = Rating.create(command.rating); + if (ratingResult.isErr) { + throw new ValidationException(ratingResult.unwrapErr()); + } + const rating = ratingResult.unwrap(); + + // Prevent self-review + if (command.userId === command.targetId && command.targetType === 'user') { + throw new ValidationException('Không thể tự đánh giá bản thân'); + } + + // One review per user per target + const existing = await this.reviewRepo.findByUserAndTarget( + command.userId, + command.targetType, + command.targetId, + ); + if (existing) { + throw new ConflictException('Bạn đã đánh giá mục tiêu này rồi'); + } + + const id = createId(); + const review = ReviewEntity.createNew( + id, + command.userId, + command.targetType, + command.targetId, + rating, + command.comment, + ); + + await this.reviewRepo.save(review); + + // Publish domain events + const events = review.clearDomainEvents(); + for (const event of events) { + this.eventBus.publish(event); + } + + this.logger.log(`Review ${id} created by user ${command.userId} for ${command.targetType}:${command.targetId}`, 'CreateReviewHandler'); + + return { + id, + rating: rating.value, + targetType: command.targetType, + targetId: command.targetId, + createdAt: review.createdAt.toISOString(), + }; + } catch (error) { + if (error instanceof DomainException) throw error; + this.logger.error( + `Failed to create review: ${error instanceof Error ? error.message : String(error)}`, + error instanceof Error ? error.stack : undefined, + 'CreateReviewHandler', + ); + throw new InternalServerErrorException('Lỗi khi tạo đánh giá'); } - const rating = ratingResult.unwrap(); - - // Prevent self-review - if (command.userId === command.targetId && command.targetType === 'user') { - throw new ValidationException('Không thể tự đánh giá bản thân'); - } - - // One review per user per target - const existing = await this.reviewRepo.findByUserAndTarget( - command.userId, - command.targetType, - command.targetId, - ); - if (existing) { - throw new ConflictException('Bạn đã đánh giá mục tiêu này rồi'); - } - - const id = createId(); - const review = ReviewEntity.createNew( - id, - command.userId, - command.targetType, - command.targetId, - rating, - command.comment, - ); - - await this.reviewRepo.save(review); - - // Publish domain events - const events = review.clearDomainEvents(); - for (const event of events) { - this.eventBus.publish(event); - } - - this.logger.log(`Review ${id} created by user ${command.userId} for ${command.targetType}:${command.targetId}`, 'CreateReviewHandler'); - - return { - id, - rating: rating.value, - targetType: command.targetType, - targetId: command.targetId, - createdAt: review.createdAt.toISOString(), - }; } } diff --git a/apps/api/src/modules/reviews/application/commands/delete-review/delete-review.handler.ts b/apps/api/src/modules/reviews/application/commands/delete-review/delete-review.handler.ts index e4e6257..fc592c6 100644 --- a/apps/api/src/modules/reviews/application/commands/delete-review/delete-review.handler.ts +++ b/apps/api/src/modules/reviews/application/commands/delete-review/delete-review.handler.ts @@ -1,8 +1,8 @@ -import { Inject } from '@nestjs/common'; +import { Inject, InternalServerErrorException } from '@nestjs/common'; // eslint-disable-next-line @typescript-eslint/consistent-type-imports -- NestJS DI requires value imports for emitDecoratorMetadata import { CommandHandler, EventBus, ICommandHandler } from '@nestjs/cqrs'; // eslint-disable-next-line @typescript-eslint/consistent-type-imports -- NestJS DI requires value imports for emitDecoratorMetadata -import { ForbiddenException, NotFoundException, LoggerService } from '@modules/shared'; +import { DomainException, ForbiddenException, NotFoundException, LoggerService } from '@modules/shared'; import { REVIEW_REPOSITORY, type IReviewRepository } from '../../../domain/repositories/review.repository'; import { DeleteReviewCommand } from './delete-review.command'; @@ -15,24 +15,34 @@ export class DeleteReviewHandler implements ICommandHandler ) {} async execute(command: DeleteReviewCommand): Promise { - const review = await this.reviewRepo.findById(command.reviewId); - if (!review) { - throw new NotFoundException('Review', command.reviewId); + try { + const review = await this.reviewRepo.findById(command.reviewId); + if (!review) { + throw new NotFoundException('Review', command.reviewId); + } + + if (review.userId !== command.userId) { + throw new ForbiddenException('Bạn chỉ có thể xóa đánh giá của chính mình'); + } + + review.markDeleted(); + await this.reviewRepo.delete(command.reviewId); + + // Publish domain events + const events = review.clearDomainEvents(); + for (const event of events) { + this.eventBus.publish(event); + } + + this.logger.log(`Review ${command.reviewId} deleted by user ${command.userId}`, 'DeleteReviewHandler'); + } catch (error) { + if (error instanceof DomainException) throw error; + this.logger.error( + `Failed to delete review: ${error instanceof Error ? error.message : String(error)}`, + error instanceof Error ? error.stack : undefined, + 'DeleteReviewHandler', + ); + throw new InternalServerErrorException('Lỗi khi xóa đánh giá'); } - - if (review.userId !== command.userId) { - throw new ForbiddenException('Bạn chỉ có thể xóa đánh giá của chính mình'); - } - - review.markDeleted(); - await this.reviewRepo.delete(command.reviewId); - - // Publish domain events - const events = review.clearDomainEvents(); - for (const event of events) { - this.eventBus.publish(event); - } - - this.logger.log(`Review ${command.reviewId} deleted by user ${command.userId}`, 'DeleteReviewHandler'); } } diff --git a/apps/api/src/modules/reviews/application/queries/get-average-rating/get-average-rating.handler.ts b/apps/api/src/modules/reviews/application/queries/get-average-rating/get-average-rating.handler.ts index b9e0f6c..5dd0098 100644 --- a/apps/api/src/modules/reviews/application/queries/get-average-rating/get-average-rating.handler.ts +++ b/apps/api/src/modules/reviews/application/queries/get-average-rating/get-average-rating.handler.ts @@ -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 ReviewStatsData } from '../../../domain/repositories/review-read.dto'; import { REVIEW_REPOSITORY, type IReviewRepository } from '../../../domain/repositories/review.repository'; import { GetAverageRatingQuery } from './get-average-rating.query'; @@ -8,9 +9,20 @@ import { GetAverageRatingQuery } from './get-average-rating.query'; export class GetAverageRatingHandler implements IQueryHandler { constructor( @Inject(REVIEW_REPOSITORY) private readonly reviewRepo: IReviewRepository, + private readonly logger: LoggerService, ) {} async execute(query: GetAverageRatingQuery): Promise { - return this.reviewRepo.getStats(query.targetType, query.targetId); + try { + return await this.reviewRepo.getStats(query.targetType, query.targetId); + } catch (error) { + if (error instanceof DomainException) throw error; + this.logger.error( + `Failed to get average rating: ${error instanceof Error ? error.message : String(error)}`, + error instanceof Error ? error.stack : undefined, + 'GetAverageRatingHandler', + ); + throw new InternalServerErrorException('Lỗi khi lấy đánh giá trung bình'); + } } } diff --git a/apps/api/src/modules/reviews/application/queries/get-reviews-by-target/get-reviews-by-target.handler.ts b/apps/api/src/modules/reviews/application/queries/get-reviews-by-target/get-reviews-by-target.handler.ts index 42dbeab..dfcdfb5 100644 --- a/apps/api/src/modules/reviews/application/queries/get-reviews-by-target/get-reviews-by-target.handler.ts +++ b/apps/api/src/modules/reviews/application/queries/get-reviews-by-target/get-reviews-by-target.handler.ts @@ -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 ReviewItemData } from '../../../domain/repositories/review-read.dto'; import { REVIEW_REPOSITORY, type IReviewRepository, type PaginatedResult } from '../../../domain/repositories/review.repository'; import { GetReviewsByTargetQuery } from './get-reviews-by-target.query'; @@ -8,14 +9,25 @@ import { GetReviewsByTargetQuery } from './get-reviews-by-target.query'; export class GetReviewsByTargetHandler implements IQueryHandler { constructor( @Inject(REVIEW_REPOSITORY) private readonly reviewRepo: IReviewRepository, + private readonly logger: LoggerService, ) {} async execute(query: GetReviewsByTargetQuery): Promise> { - return this.reviewRepo.findByTarget( - query.targetType, - query.targetId, - query.page, - query.limit, - ); + try { + return await this.reviewRepo.findByTarget( + query.targetType, + query.targetId, + query.page, + query.limit, + ); + } catch (error) { + if (error instanceof DomainException) throw error; + this.logger.error( + `Failed to get reviews by target: ${error instanceof Error ? error.message : String(error)}`, + error instanceof Error ? error.stack : undefined, + 'GetReviewsByTargetHandler', + ); + throw new InternalServerErrorException('Lỗi khi lấy danh sách đánh giá'); + } } } diff --git a/apps/api/src/modules/reviews/application/queries/get-reviews-by-user/get-reviews-by-user.handler.ts b/apps/api/src/modules/reviews/application/queries/get-reviews-by-user/get-reviews-by-user.handler.ts index 4ff0eed..e0c9b14 100644 --- a/apps/api/src/modules/reviews/application/queries/get-reviews-by-user/get-reviews-by-user.handler.ts +++ b/apps/api/src/modules/reviews/application/queries/get-reviews-by-user/get-reviews-by-user.handler.ts @@ -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 ReviewItemData } from '../../../domain/repositories/review-read.dto'; import { REVIEW_REPOSITORY, type IReviewRepository, type PaginatedResult } from '../../../domain/repositories/review.repository'; import { GetReviewsByUserQuery } from './get-reviews-by-user.query'; @@ -8,9 +9,20 @@ import { GetReviewsByUserQuery } from './get-reviews-by-user.query'; export class GetReviewsByUserHandler implements IQueryHandler { constructor( @Inject(REVIEW_REPOSITORY) private readonly reviewRepo: IReviewRepository, + private readonly logger: LoggerService, ) {} async execute(query: GetReviewsByUserQuery): Promise> { - return this.reviewRepo.findByUserId(query.userId, query.page, query.limit); + try { + return await this.reviewRepo.findByUserId(query.userId, query.page, query.limit); + } catch (error) { + if (error instanceof DomainException) throw error; + this.logger.error( + `Failed to get reviews by user: ${error instanceof Error ? error.message : String(error)}`, + error instanceof Error ? error.stack : undefined, + 'GetReviewsByUserHandler', + ); + throw new InternalServerErrorException('Lỗi khi lấy danh sách đánh giá'); + } } }