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 { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
import { type PlanTier } from '@prisma/client'; 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 { SUBSCRIPTION_REPOSITORY, type ISubscriptionRepository } from '@modules/subscriptions';
import { SubscriptionAdjustedEvent } from '../../../domain/events/subscription-adjusted.event'; import { SubscriptionAdjustedEvent } from '../../../domain/events/subscription-adjusted.event';
import { AdjustSubscriptionCommand } from './adjust-subscription.command'; import { AdjustSubscriptionCommand } from './adjust-subscription.command';
@@ -20,9 +20,11 @@ export class AdjustSubscriptionHandler implements ICommandHandler<AdjustSubscrip
@Inject(SUBSCRIPTION_REPOSITORY) private readonly subscriptionRepo: ISubscriptionRepository, @Inject(SUBSCRIPTION_REPOSITORY) private readonly subscriptionRepo: ISubscriptionRepository,
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly eventBus: EventBus, private readonly eventBus: EventBus,
private readonly logger: LoggerService,
) {} ) {}
async execute(command: AdjustSubscriptionCommand): Promise<AdjustSubscriptionResult> { async execute(command: AdjustSubscriptionCommand): Promise<AdjustSubscriptionResult> {
try {
const tier = command.newPlanTier as PlanTier; const tier = command.newPlanTier as PlanTier;
if (!VALID_TIERS.includes(tier)) { if (!VALID_TIERS.includes(tier)) {
throw new ValidationException(`Gói không hợp lệ: ${command.newPlanTier}`, { throw new ValidationException(`Gói không hợp lệ: ${command.newPlanTier}`, {
@@ -63,5 +65,14 @@ export class AdjustSubscriptionHandler implements ICommandHandler<AdjustSubscrip
newPlanTier: tier, newPlanTier: tier,
message: `Subscription đã được chuyển sang gói ${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');
}
} }
} }

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 { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
import { USER_REPOSITORY, type IUserRepository } from '@modules/auth'; 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 { KycApprovedEvent } from '../../../domain/events/kyc-approved.event';
import { ApproveKycCommand } from './approve-kyc.command'; import { ApproveKycCommand } from './approve-kyc.command';
@@ -16,9 +16,11 @@ export class ApproveKycHandler implements ICommandHandler<ApproveKycCommand> {
constructor( constructor(
@Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository, @Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository,
private readonly eventBus: EventBus, private readonly eventBus: EventBus,
private readonly logger: LoggerService,
) {} ) {}
async execute(command: ApproveKycCommand): Promise<ApproveKycResult> { async execute(command: ApproveKycCommand): Promise<ApproveKycResult> {
try {
const user = await this.userRepo.findById(command.userId); const user = await this.userRepo.findById(command.userId);
if (!user) { if (!user) {
throw new NotFoundException('Người dùng không tồn tại'); throw new NotFoundException('Người dùng không tồn tại');
@@ -41,5 +43,14 @@ export class ApproveKycHandler implements ICommandHandler<ApproveKycCommand> {
kycStatus: 'VERIFIED', kycStatus: 'VERIFIED',
message: 'KYC đã được duyệt thành công', 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');
}
} }
} }

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 { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
import { LISTING_REPOSITORY, type IListingRepository } from '@modules/listings'; 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 { ListingApprovedEvent } from '../../../domain/events/listing-approved.event';
import { ApproveListingCommand } from './approve-listing.command'; import { ApproveListingCommand } from './approve-listing.command';
@@ -16,9 +16,11 @@ export class ApproveListingHandler implements ICommandHandler<ApproveListingComm
constructor( constructor(
@Inject(LISTING_REPOSITORY) private readonly listingRepo: IListingRepository, @Inject(LISTING_REPOSITORY) private readonly listingRepo: IListingRepository,
private readonly eventBus: EventBus, private readonly eventBus: EventBus,
private readonly logger: LoggerService,
) {} ) {}
async execute(command: ApproveListingCommand): Promise<ApproveListingResult> { async execute(command: ApproveListingCommand): Promise<ApproveListingResult> {
try {
const listing = await this.listingRepo.findById(command.listingId); const listing = await this.listingRepo.findById(command.listingId);
if (!listing) { if (!listing) {
throw new NotFoundException('Listing không tồn tại'); throw new NotFoundException('Listing không tồn tại');
@@ -48,5 +50,14 @@ export class ApproveListingHandler implements ICommandHandler<ApproveListingComm
status: 'ACTIVE', status: 'ACTIVE',
message: 'Listing đã được duyệt thành công', 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');
}
} }
} }

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 { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
import { USER_REPOSITORY, type IUserRepository } from '@modules/auth'; 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 { UserBannedEvent } from '../../../domain/events/user-banned.event';
import { UserUnbannedEvent } from '../../../domain/events/user-unbanned.event'; import { UserUnbannedEvent } from '../../../domain/events/user-unbanned.event';
import { BanUserCommand } from './ban-user.command'; import { BanUserCommand } from './ban-user.command';
@@ -17,9 +17,11 @@ export class BanUserHandler implements ICommandHandler<BanUserCommand> {
constructor( constructor(
@Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository, @Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository,
private readonly eventBus: EventBus, private readonly eventBus: EventBus,
private readonly logger: LoggerService,
) {} ) {}
async execute(command: BanUserCommand): Promise<BanUserResult> { async execute(command: BanUserCommand): Promise<BanUserResult> {
try {
const user = await this.userRepo.findById(command.userId); const user = await this.userRepo.findById(command.userId);
if (!user) { if (!user) {
throw new NotFoundException('Người dùng không tồn tại'); throw new NotFoundException('Người dùng không tồn tại');
@@ -66,5 +68,14 @@ export class BanUserHandler implements ICommandHandler<BanUserCommand> {
isActive: false, isActive: false,
message: 'Người dùng đã bị ban', 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');
}
} }
} }

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 { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
import { LISTING_REPOSITORY, type IListingRepository } from '@modules/listings'; 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 { ListingApprovedEvent } from '../../../domain/events/listing-approved.event';
import { ListingRejectedEvent } from '../../../domain/events/listing-rejected.event'; import { ListingRejectedEvent } from '../../../domain/events/listing-rejected.event';
import { BulkModerateListingsCommand } from './bulk-moderate-listings.command'; import { BulkModerateListingsCommand } from './bulk-moderate-listings.command';
@@ -17,9 +17,11 @@ export class BulkModerateListingsHandler implements ICommandHandler<BulkModerate
constructor( constructor(
@Inject(LISTING_REPOSITORY) private readonly listingRepo: IListingRepository, @Inject(LISTING_REPOSITORY) private readonly listingRepo: IListingRepository,
private readonly eventBus: EventBus, private readonly eventBus: EventBus,
private readonly logger: LoggerService,
) {} ) {}
async execute(command: BulkModerateListingsCommand): Promise<BulkModerateResult> { async execute(command: BulkModerateListingsCommand): Promise<BulkModerateResult> {
try {
if (command.listingIds.length === 0) { if (command.listingIds.length === 0) {
throw new ValidationException('Danh sách listing không được rỗng', {}); throw new ValidationException('Danh sách listing không được rỗng', {});
} }
@@ -72,5 +74,14 @@ export class BulkModerateListingsHandler implements ICommandHandler<BulkModerate
succeeded, succeeded,
failed, 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');
}
} }
} }

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 { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
import { USER_REPOSITORY, type IUserRepository } from '@modules/auth'; 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 { KycRejectedEvent } from '../../../domain/events/kyc-rejected.event';
import { RejectKycCommand } from './reject-kyc.command'; import { RejectKycCommand } from './reject-kyc.command';
@@ -16,9 +16,11 @@ export class RejectKycHandler implements ICommandHandler<RejectKycCommand> {
constructor( constructor(
@Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository, @Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository,
private readonly eventBus: EventBus, private readonly eventBus: EventBus,
private readonly logger: LoggerService,
) {} ) {}
async execute(command: RejectKycCommand): Promise<RejectKycResult> { async execute(command: RejectKycCommand): Promise<RejectKycResult> {
try {
const user = await this.userRepo.findById(command.userId); const user = await this.userRepo.findById(command.userId);
if (!user) { if (!user) {
throw new NotFoundException('Người dùng không tồn tại'); throw new NotFoundException('Người dùng không tồn tại');
@@ -41,5 +43,14 @@ export class RejectKycHandler implements ICommandHandler<RejectKycCommand> {
kycStatus: 'REJECTED', kycStatus: 'REJECTED',
message: 'KYC đã bị từ chối', 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');
}
} }
} }

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 { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
import { LISTING_REPOSITORY, type IListingRepository } from '@modules/listings'; 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 { ListingRejectedEvent } from '../../../domain/events/listing-rejected.event';
import { RejectListingCommand } from './reject-listing.command'; import { RejectListingCommand } from './reject-listing.command';
@@ -16,9 +16,11 @@ export class RejectListingHandler implements ICommandHandler<RejectListingComman
constructor( constructor(
@Inject(LISTING_REPOSITORY) private readonly listingRepo: IListingRepository, @Inject(LISTING_REPOSITORY) private readonly listingRepo: IListingRepository,
private readonly eventBus: EventBus, private readonly eventBus: EventBus,
private readonly logger: LoggerService,
) {} ) {}
async execute(command: RejectListingCommand): Promise<RejectListingResult> { async execute(command: RejectListingCommand): Promise<RejectListingResult> {
try {
const listing = await this.listingRepo.findById(command.listingId); const listing = await this.listingRepo.findById(command.listingId);
if (!listing) { if (!listing) {
throw new NotFoundException('Listing không tồn tại'); throw new NotFoundException('Listing không tồn tại');
@@ -43,5 +45,14 @@ export class RejectListingHandler implements ICommandHandler<RejectListingComman
status: 'REJECTED', status: 'REJECTED',
message: 'Listing đã bị từ chối', 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');
}
} }
} }

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 { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
import { USER_REPOSITORY, type IUserRepository } from '@modules/auth'; 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 { UserBannedEvent } from '../../../domain/events/user-banned.event';
import { UserUnbannedEvent } from '../../../domain/events/user-unbanned.event'; import { UserUnbannedEvent } from '../../../domain/events/user-unbanned.event';
import { UpdateUserStatusCommand } from './update-user-status.command'; import { UpdateUserStatusCommand } from './update-user-status.command';
@@ -17,9 +17,11 @@ export class UpdateUserStatusHandler implements ICommandHandler<UpdateUserStatus
constructor( constructor(
@Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository, @Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository,
private readonly eventBus: EventBus, private readonly eventBus: EventBus,
private readonly logger: LoggerService,
) {} ) {}
async execute(command: UpdateUserStatusCommand): Promise<UpdateUserStatusResult> { async execute(command: UpdateUserStatusCommand): Promise<UpdateUserStatusResult> {
try {
const user = await this.userRepo.findById(command.userId); const user = await this.userRepo.findById(command.userId);
if (!user) { if (!user) {
throw new NotFoundException('Người dùng không tồn tại'); throw new NotFoundException('Người dùng không tồn tại');
@@ -59,5 +61,14 @@ export class UpdateUserStatusHandler implements ICommandHandler<UpdateUserStatus
isActive: false, isActive: false,
message: 'Người dùng đã bị vô hiệu hóa', 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');
}
} }
} }

View File

@@ -1,5 +1,6 @@
import { Inject } from '@nestjs/common'; import { Inject, InternalServerErrorException } from '@nestjs/common';
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs'; import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';
import { DomainException, type LoggerService } from '@modules/shared';
import { import {
AUDIT_LOG_REPOSITORY, AUDIT_LOG_REPOSITORY,
type IAuditLogRepository, type IAuditLogRepository,
@@ -11,10 +12,12 @@ import { GetAuditLogsQuery } from './get-audit-logs.query';
export class GetAuditLogsHandler implements IQueryHandler<GetAuditLogsQuery> { export class GetAuditLogsHandler implements IQueryHandler<GetAuditLogsQuery> {
constructor( constructor(
@Inject(AUDIT_LOG_REPOSITORY) private readonly auditRepo: IAuditLogRepository, @Inject(AUDIT_LOG_REPOSITORY) private readonly auditRepo: IAuditLogRepository,
private readonly logger: LoggerService,
) {} ) {}
async execute(query: GetAuditLogsQuery): Promise<AuditLogListResult> { async execute(query: GetAuditLogsQuery): Promise<AuditLogListResult> {
return this.auditRepo.findAll({ try {
return await this.auditRepo.findAll({
page: query.page, page: query.page,
limit: query.limit, limit: query.limit,
action: query.action, action: query.action,
@@ -24,5 +27,14 @@ export class GetAuditLogsHandler implements IQueryHandler<GetAuditLogsQuery> {
startDate: query.startDate, startDate: query.startDate,
endDate: query.endDate, 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');
}
} }
} }

View File

@@ -1,5 +1,6 @@
import { Inject } from '@nestjs/common'; import { Inject, InternalServerErrorException } from '@nestjs/common';
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs'; 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 { ADMIN_QUERY_REPOSITORY, type IAdminQueryRepository, type DashboardStats } from '../../../domain/repositories/admin-query.repository';
import { GetDashboardStatsQuery } from './get-dashboard-stats.query'; import { GetDashboardStatsQuery } from './get-dashboard-stats.query';
@@ -7,9 +8,20 @@ import { GetDashboardStatsQuery } from './get-dashboard-stats.query';
export class GetDashboardStatsHandler implements IQueryHandler<GetDashboardStatsQuery> { export class GetDashboardStatsHandler implements IQueryHandler<GetDashboardStatsQuery> {
constructor( constructor(
@Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository, @Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository,
private readonly logger: LoggerService,
) {} ) {}
async execute(_query: GetDashboardStatsQuery): Promise<DashboardStats> { async execute(_query: GetDashboardStatsQuery): Promise<DashboardStats> {
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');
}
} }
} }

View File

@@ -1,5 +1,6 @@
import { Inject } from '@nestjs/common'; import { Inject, InternalServerErrorException } from '@nestjs/common';
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs'; 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 { ADMIN_QUERY_REPOSITORY, type IAdminQueryRepository, type KycQueueResult } from '../../../domain/repositories/admin-query.repository';
import { GetKycQueueQuery } from './get-kyc-queue.query'; import { GetKycQueueQuery } from './get-kyc-queue.query';
@@ -7,9 +8,20 @@ import { GetKycQueueQuery } from './get-kyc-queue.query';
export class GetKycQueueHandler implements IQueryHandler<GetKycQueueQuery> { export class GetKycQueueHandler implements IQueryHandler<GetKycQueueQuery> {
constructor( constructor(
@Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository, @Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository,
private readonly logger: LoggerService,
) {} ) {}
async execute(query: GetKycQueueQuery): Promise<KycQueueResult> { async execute(query: GetKycQueueQuery): Promise<KycQueueResult> {
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');
}
} }
} }

View File

@@ -1,5 +1,6 @@
import { Inject } from '@nestjs/common'; import { Inject, InternalServerErrorException } from '@nestjs/common';
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs'; 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 { ADMIN_QUERY_REPOSITORY, type IAdminQueryRepository, type ModerationQueueResult } from '../../../domain/repositories/admin-query.repository';
import { GetModerationQueueQuery } from './get-moderation-queue.query'; import { GetModerationQueueQuery } from './get-moderation-queue.query';
@@ -7,9 +8,20 @@ import { GetModerationQueueQuery } from './get-moderation-queue.query';
export class GetModerationQueueHandler implements IQueryHandler<GetModerationQueueQuery> { export class GetModerationQueueHandler implements IQueryHandler<GetModerationQueueQuery> {
constructor( constructor(
@Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository, @Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository,
private readonly logger: LoggerService,
) {} ) {}
async execute(query: GetModerationQueueQuery): Promise<ModerationQueueResult> { async execute(query: GetModerationQueueQuery): Promise<ModerationQueueResult> {
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');
}
} }
} }

View File

@@ -1,5 +1,6 @@
import { Inject } from '@nestjs/common'; import { Inject, InternalServerErrorException } from '@nestjs/common';
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs'; 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 { ADMIN_QUERY_REPOSITORY, type IAdminQueryRepository, type RevenueStatsItem } from '../../../domain/repositories/admin-query.repository';
import { GetRevenueStatsQuery } from './get-revenue-stats.query'; import { GetRevenueStatsQuery } from './get-revenue-stats.query';
@@ -7,9 +8,20 @@ import { GetRevenueStatsQuery } from './get-revenue-stats.query';
export class GetRevenueStatsHandler implements IQueryHandler<GetRevenueStatsQuery> { export class GetRevenueStatsHandler implements IQueryHandler<GetRevenueStatsQuery> {
constructor( constructor(
@Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository, @Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository,
private readonly logger: LoggerService,
) {} ) {}
async execute(query: GetRevenueStatsQuery): Promise<RevenueStatsItem[]> { async execute(query: GetRevenueStatsQuery): Promise<RevenueStatsItem[]> {
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');
}
} }
} }

View File

@@ -1,6 +1,6 @@
import { Inject } from '@nestjs/common'; import { Inject, InternalServerErrorException } from '@nestjs/common';
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs'; 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 { ADMIN_QUERY_REPOSITORY, type IAdminQueryRepository, type UserDetail } from '../../../domain/repositories/admin-query.repository';
import { GetUserDetailQuery } from './get-user-detail.query'; import { GetUserDetailQuery } from './get-user-detail.query';
@@ -8,13 +8,24 @@ import { GetUserDetailQuery } from './get-user-detail.query';
export class GetUserDetailHandler implements IQueryHandler<GetUserDetailQuery> { export class GetUserDetailHandler implements IQueryHandler<GetUserDetailQuery> {
constructor( constructor(
@Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository, @Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository,
private readonly logger: LoggerService,
) {} ) {}
async execute(query: GetUserDetailQuery): Promise<UserDetail> { async execute(query: GetUserDetailQuery): Promise<UserDetail> {
try {
const user = await this.adminQueryRepo.getUserDetail(query.userId); const user = await this.adminQueryRepo.getUserDetail(query.userId);
if (!user) { if (!user) {
throw new NotFoundException('Người dùng không tồn tại'); throw new NotFoundException('Người dùng không tồn tại');
} }
return user; 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');
}
} }
} }

View File

@@ -1,5 +1,6 @@
import { Inject } from '@nestjs/common'; import { Inject, InternalServerErrorException } from '@nestjs/common';
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs'; 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 { ADMIN_QUERY_REPOSITORY, type IAdminQueryRepository, type UserListResult } from '../../../domain/repositories/admin-query.repository';
import { GetUsersQuery } from './get-users.query'; import { GetUsersQuery } from './get-users.query';
@@ -7,15 +8,26 @@ import { GetUsersQuery } from './get-users.query';
export class GetUsersHandler implements IQueryHandler<GetUsersQuery> { export class GetUsersHandler implements IQueryHandler<GetUsersQuery> {
constructor( constructor(
@Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository, @Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository,
private readonly logger: LoggerService,
) {} ) {}
async execute(query: GetUsersQuery): Promise<UserListResult> { async execute(query: GetUsersQuery): Promise<UserListResult> {
return this.adminQueryRepo.getUsers({ try {
return await this.adminQueryRepo.getUsers({
page: query.page, page: query.page,
limit: query.limit, limit: query.limit,
role: query.role, role: query.role,
isActive: query.isActive, isActive: query.isActive,
search: query.search, 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');
}
} }
} }

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 { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
import { createId } from '@paralleldrive/cuid2'; 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 { InquiryEntity } from '../../../domain/entities/inquiry.entity';
import { INQUIRY_REPOSITORY, type IInquiryRepository } from '../../../domain/repositories/inquiry.repository'; import { INQUIRY_REPOSITORY, type IInquiryRepository } from '../../../domain/repositories/inquiry.repository';
import { CreateInquiryCommand } from './create-inquiry.command'; import { CreateInquiryCommand } from './create-inquiry.command';
@@ -22,6 +22,7 @@ export class CreateInquiryHandler implements ICommandHandler<CreateInquiryComman
) {} ) {}
async execute(command: CreateInquiryCommand): Promise<CreateInquiryResult> { async execute(command: CreateInquiryCommand): Promise<CreateInquiryResult> {
try {
// Validate listing exists // Validate listing exists
const listing = await this.prisma.listing.findUnique({ const listing = await this.prisma.listing.findUnique({
where: { id: command.listingId }, where: { id: command.listingId },
@@ -55,5 +56,14 @@ export class CreateInquiryHandler implements ICommandHandler<CreateInquiryComman
listingId: command.listingId, listingId: command.listingId,
createdAt: inquiry.createdAt.toISOString(), 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');
}
} }
} }

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 { 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 { INQUIRY_REPOSITORY, type IInquiryRepository } from '../../../domain/repositories/inquiry.repository';
import { MarkInquiryReadCommand } from './mark-inquiry-read.command'; import { MarkInquiryReadCommand } from './mark-inquiry-read.command';
@@ -14,6 +14,7 @@ export class MarkInquiryReadHandler implements ICommandHandler<MarkInquiryReadCo
) {} ) {}
async execute(command: MarkInquiryReadCommand): Promise<void> { async execute(command: MarkInquiryReadCommand): Promise<void> {
try {
const inquiry = await this.inquiryRepo.findById(command.inquiryId); const inquiry = await this.inquiryRepo.findById(command.inquiryId);
if (!inquiry) { if (!inquiry) {
throw new NotFoundException('Inquiry', command.inquiryId); throw new NotFoundException('Inquiry', command.inquiryId);
@@ -46,5 +47,14 @@ export class MarkInquiryReadHandler implements ICommandHandler<MarkInquiryReadCo
} }
this.logger.log(`Inquiry ${command.inquiryId} marked as read by agent ${command.agentUserId}`, 'MarkInquiryReadHandler'); 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');
}
} }
} }

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 { 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 { type InquiryReadDto } from '../../../domain/repositories/inquiry-read.dto';
import { INQUIRY_REPOSITORY, type IInquiryRepository, type PaginatedResult } from '../../../domain/repositories/inquiry.repository'; import { INQUIRY_REPOSITORY, type IInquiryRepository, type PaginatedResult } from '../../../domain/repositories/inquiry.repository';
import { GetInquiriesByAgentQuery } from './get-inquiries-by-agent.query'; import { GetInquiriesByAgentQuery } from './get-inquiries-by-agent.query';
@@ -10,9 +10,11 @@ export class GetInquiriesByAgentHandler implements IQueryHandler<GetInquiriesByA
constructor( constructor(
@Inject(INQUIRY_REPOSITORY) private readonly inquiryRepo: IInquiryRepository, @Inject(INQUIRY_REPOSITORY) private readonly inquiryRepo: IInquiryRepository,
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly logger: LoggerService,
) {} ) {}
async execute(query: GetInquiriesByAgentQuery): Promise<PaginatedResult<InquiryReadDto>> { async execute(query: GetInquiriesByAgentQuery): Promise<PaginatedResult<InquiryReadDto>> {
try {
const agent = await this.prisma.agent.findUnique({ const agent = await this.prisma.agent.findUnique({
where: { userId: query.agentUserId }, where: { userId: query.agentUserId },
select: { id: true }, select: { id: true },
@@ -26,5 +28,14 @@ export class GetInquiriesByAgentHandler implements IQueryHandler<GetInquiriesByA
query.page, query.page,
query.limit, 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 { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { DomainException, type LoggerService } from '@modules/shared';
import { type InquiryReadDto } from '../../../domain/repositories/inquiry-read.dto'; import { type InquiryReadDto } from '../../../domain/repositories/inquiry-read.dto';
import { INQUIRY_REPOSITORY, type IInquiryRepository, type PaginatedResult } from '../../../domain/repositories/inquiry.repository'; import { INQUIRY_REPOSITORY, type IInquiryRepository, type PaginatedResult } from '../../../domain/repositories/inquiry.repository';
import { GetInquiriesByListingQuery } from './get-inquiries-by-listing.query'; 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> { export class GetInquiriesByListingHandler implements IQueryHandler<GetInquiriesByListingQuery> {
constructor( constructor(
@Inject(INQUIRY_REPOSITORY) private readonly inquiryRepo: IInquiryRepository, @Inject(INQUIRY_REPOSITORY) private readonly inquiryRepo: IInquiryRepository,
private readonly logger: LoggerService,
) {} ) {}
async execute(query: GetInquiriesByListingQuery): Promise<PaginatedResult<InquiryReadDto>> { async execute(query: GetInquiriesByListingQuery): Promise<PaginatedResult<InquiryReadDto>> {
return this.inquiryRepo.findByListing( try {
return await this.inquiryRepo.findByListing(
query.listingId, query.listingId,
query.page, query.page,
query.limit, 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');
}
} }
} }

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 { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
import { createId } from '@paralleldrive/cuid2'; 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 { LeadEntity } from '../../../domain/entities/lead.entity';
import { LEAD_REPOSITORY, type ILeadRepository } from '../../../domain/repositories/lead.repository'; import { LEAD_REPOSITORY, type ILeadRepository } from '../../../domain/repositories/lead.repository';
import { LeadScore } from '../../../domain/value-objects/lead-score.vo'; import { LeadScore } from '../../../domain/value-objects/lead-score.vo';
@@ -23,6 +23,7 @@ export class CreateLeadHandler implements ICommandHandler<CreateLeadCommand> {
) {} ) {}
async execute(command: CreateLeadCommand): Promise<CreateLeadResult> { async execute(command: CreateLeadCommand): Promise<CreateLeadResult> {
try {
// Look up agent by userId // Look up agent by userId
const agent = await this.prisma.agent.findUnique({ const agent = await this.prisma.agent.findUnique({
where: { userId: command.agentUserId }, where: { userId: command.agentUserId },
@@ -68,5 +69,14 @@ export class CreateLeadHandler implements ICommandHandler<CreateLeadCommand> {
status: lead.status, status: lead.status,
createdAt: lead.createdAt.toISOString(), 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');
}
} }
} }

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 { 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 { LEAD_REPOSITORY, type ILeadRepository } from '../../../domain/repositories/lead.repository';
import { DeleteLeadCommand } from './delete-lead.command'; import { DeleteLeadCommand } from './delete-lead.command';
@@ -14,6 +14,7 @@ export class DeleteLeadHandler implements ICommandHandler<DeleteLeadCommand> {
) {} ) {}
async execute(command: DeleteLeadCommand): Promise<void> { async execute(command: DeleteLeadCommand): Promise<void> {
try {
// Look up agent by userId // Look up agent by userId
const agent = await this.prisma.agent.findUnique({ const agent = await this.prisma.agent.findUnique({
where: { userId: command.agentUserId }, where: { userId: command.agentUserId },
@@ -35,5 +36,14 @@ export class DeleteLeadHandler implements ICommandHandler<DeleteLeadCommand> {
await this.leadRepo.delete(command.leadId); await this.leadRepo.delete(command.leadId);
this.logger.log(`Lead ${command.leadId} deleted by agent ${agent.id}`, 'DeleteLeadHandler'); 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');
}
} }
} }

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 { 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 { type LeadStatus } from '../../../domain/entities/lead.entity';
import { LEAD_REPOSITORY, type ILeadRepository } from '../../../domain/repositories/lead.repository'; import { LEAD_REPOSITORY, type ILeadRepository } from '../../../domain/repositories/lead.repository';
import { UpdateLeadStatusCommand } from './update-lead-status.command'; import { UpdateLeadStatusCommand } from './update-lead-status.command';
@@ -15,6 +15,7 @@ export class UpdateLeadStatusHandler implements ICommandHandler<UpdateLeadStatus
) {} ) {}
async execute(command: UpdateLeadStatusCommand): Promise<void> { async execute(command: UpdateLeadStatusCommand): Promise<void> {
try {
// Look up agent by userId // Look up agent by userId
const agent = await this.prisma.agent.findUnique({ const agent = await this.prisma.agent.findUnique({
where: { userId: command.agentUserId }, where: { userId: command.agentUserId },
@@ -43,5 +44,14 @@ export class UpdateLeadStatusHandler implements ICommandHandler<UpdateLeadStatus
} }
this.logger.log(`Lead ${command.leadId} status updated to ${command.newStatus} by agent ${agent.id}`, 'UpdateLeadStatusHandler'); 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');
}
} }
} }

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 { 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 { LEAD_REPOSITORY, type ILeadRepository, type LeadStatsData } from '../../../domain/repositories/lead.repository';
import { GetLeadStatsQuery } from './get-lead-stats.query'; import { GetLeadStatsQuery } from './get-lead-stats.query';
@@ -9,9 +9,11 @@ export class GetLeadStatsHandler implements IQueryHandler<GetLeadStatsQuery> {
constructor( constructor(
@Inject(LEAD_REPOSITORY) private readonly leadRepo: ILeadRepository, @Inject(LEAD_REPOSITORY) private readonly leadRepo: ILeadRepository,
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly logger: LoggerService,
) {} ) {}
async execute(query: GetLeadStatsQuery): Promise<LeadStatsData> { async execute(query: GetLeadStatsQuery): Promise<LeadStatsData> {
try {
// Look up agent by userId // Look up agent by userId
const agent = await this.prisma.agent.findUnique({ const agent = await this.prisma.agent.findUnique({
where: { userId: query.agentUserId }, where: { userId: query.agentUserId },
@@ -21,5 +23,14 @@ export class GetLeadStatsHandler implements IQueryHandler<GetLeadStatsQuery> {
} }
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 { 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 { type LeadReadDto } from '../../../domain/repositories/lead-read.dto';
import { LEAD_REPOSITORY, type ILeadRepository, type PaginatedResult } from '../../../domain/repositories/lead.repository'; import { LEAD_REPOSITORY, type ILeadRepository, type PaginatedResult } from '../../../domain/repositories/lead.repository';
import { GetLeadsByAgentQuery } from './get-leads-by-agent.query'; import { GetLeadsByAgentQuery } from './get-leads-by-agent.query';
@@ -10,9 +10,11 @@ export class GetLeadsByAgentHandler implements IQueryHandler<GetLeadsByAgentQuer
constructor( constructor(
@Inject(LEAD_REPOSITORY) private readonly leadRepo: ILeadRepository, @Inject(LEAD_REPOSITORY) private readonly leadRepo: ILeadRepository,
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly logger: LoggerService,
) {} ) {}
async execute(query: GetLeadsByAgentQuery): Promise<PaginatedResult<LeadReadDto>> { async execute(query: GetLeadsByAgentQuery): Promise<PaginatedResult<LeadReadDto>> {
try {
// Look up agent by userId // Look up agent by userId
const agent = await this.prisma.agent.findUnique({ const agent = await this.prisma.agent.findUnique({
where: { userId: query.agentUserId }, where: { userId: query.agentUserId },
@@ -27,5 +29,14 @@ export class GetLeadsByAgentHandler implements IQueryHandler<GetLeadsByAgentQuer
query.page, query.page,
query.limit, 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');
}
} }
} }

View File

@@ -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 // eslint-disable-next-line @typescript-eslint/consistent-type-imports -- NestJS DI requires value imports for emitDecoratorMetadata
import { CommandHandler, EventBus, ICommandHandler } from '@nestjs/cqrs'; import { CommandHandler, EventBus, ICommandHandler } from '@nestjs/cqrs';
import { createId } from '@paralleldrive/cuid2'; import { createId } from '@paralleldrive/cuid2';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- NestJS DI requires value imports for emitDecoratorMetadata // 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 { ReviewEntity } from '../../../domain/entities/review.entity';
import { REVIEW_REPOSITORY, type IReviewRepository } from '../../../domain/repositories/review.repository'; import { REVIEW_REPOSITORY, type IReviewRepository } from '../../../domain/repositories/review.repository';
import { Rating } from '../../../domain/value-objects/rating.vo'; import { Rating } from '../../../domain/value-objects/rating.vo';
@@ -26,6 +26,7 @@ export class CreateReviewHandler implements ICommandHandler<CreateReviewCommand>
) {} ) {}
async execute(command: CreateReviewCommand): Promise<CreateReviewResult> { async execute(command: CreateReviewCommand): Promise<CreateReviewResult> {
try {
// Validate rating value object // Validate rating value object
const ratingResult = Rating.create(command.rating); const ratingResult = Rating.create(command.rating);
if (ratingResult.isErr) { if (ratingResult.isErr) {
@@ -75,5 +76,14 @@ export class CreateReviewHandler implements ICommandHandler<CreateReviewCommand>
targetId: command.targetId, targetId: command.targetId,
createdAt: review.createdAt.toISOString(), 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á');
}
} }
} }

View File

@@ -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 // eslint-disable-next-line @typescript-eslint/consistent-type-imports -- NestJS DI requires value imports for emitDecoratorMetadata
import { CommandHandler, EventBus, ICommandHandler } from '@nestjs/cqrs'; import { CommandHandler, EventBus, ICommandHandler } from '@nestjs/cqrs';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- NestJS DI requires value imports for emitDecoratorMetadata // 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 { REVIEW_REPOSITORY, type IReviewRepository } from '../../../domain/repositories/review.repository';
import { DeleteReviewCommand } from './delete-review.command'; import { DeleteReviewCommand } from './delete-review.command';
@@ -15,6 +15,7 @@ export class DeleteReviewHandler implements ICommandHandler<DeleteReviewCommand>
) {} ) {}
async execute(command: DeleteReviewCommand): Promise<void> { async execute(command: DeleteReviewCommand): Promise<void> {
try {
const review = await this.reviewRepo.findById(command.reviewId); const review = await this.reviewRepo.findById(command.reviewId);
if (!review) { if (!review) {
throw new NotFoundException('Review', command.reviewId); throw new NotFoundException('Review', command.reviewId);
@@ -34,5 +35,14 @@ export class DeleteReviewHandler implements ICommandHandler<DeleteReviewCommand>
} }
this.logger.log(`Review ${command.reviewId} deleted by user ${command.userId}`, 'DeleteReviewHandler'); 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á');
}
} }
} }

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 { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { DomainException, type LoggerService } from '@modules/shared';
import { type ReviewStatsData } from '../../../domain/repositories/review-read.dto'; import { type ReviewStatsData } from '../../../domain/repositories/review-read.dto';
import { REVIEW_REPOSITORY, type IReviewRepository } from '../../../domain/repositories/review.repository'; import { REVIEW_REPOSITORY, type IReviewRepository } from '../../../domain/repositories/review.repository';
import { GetAverageRatingQuery } from './get-average-rating.query'; import { GetAverageRatingQuery } from './get-average-rating.query';
@@ -8,9 +9,20 @@ import { GetAverageRatingQuery } from './get-average-rating.query';
export class GetAverageRatingHandler implements IQueryHandler<GetAverageRatingQuery> { export class GetAverageRatingHandler implements IQueryHandler<GetAverageRatingQuery> {
constructor( constructor(
@Inject(REVIEW_REPOSITORY) private readonly reviewRepo: IReviewRepository, @Inject(REVIEW_REPOSITORY) private readonly reviewRepo: IReviewRepository,
private readonly logger: LoggerService,
) {} ) {}
async execute(query: GetAverageRatingQuery): Promise<ReviewStatsData> { async execute(query: GetAverageRatingQuery): Promise<ReviewStatsData> {
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');
}
} }
} }

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 { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { DomainException, type LoggerService } from '@modules/shared';
import { type ReviewItemData } from '../../../domain/repositories/review-read.dto'; import { type ReviewItemData } from '../../../domain/repositories/review-read.dto';
import { REVIEW_REPOSITORY, type IReviewRepository, type PaginatedResult } from '../../../domain/repositories/review.repository'; import { REVIEW_REPOSITORY, type IReviewRepository, type PaginatedResult } from '../../../domain/repositories/review.repository';
import { GetReviewsByTargetQuery } from './get-reviews-by-target.query'; 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<GetReviewsByTargetQuery> { export class GetReviewsByTargetHandler implements IQueryHandler<GetReviewsByTargetQuery> {
constructor( constructor(
@Inject(REVIEW_REPOSITORY) private readonly reviewRepo: IReviewRepository, @Inject(REVIEW_REPOSITORY) private readonly reviewRepo: IReviewRepository,
private readonly logger: LoggerService,
) {} ) {}
async execute(query: GetReviewsByTargetQuery): Promise<PaginatedResult<ReviewItemData>> { async execute(query: GetReviewsByTargetQuery): Promise<PaginatedResult<ReviewItemData>> {
return this.reviewRepo.findByTarget( try {
return await this.reviewRepo.findByTarget(
query.targetType, query.targetType,
query.targetId, query.targetId,
query.page, query.page,
query.limit, 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á');
}
} }
} }

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 { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { DomainException, type LoggerService } from '@modules/shared';
import { type ReviewItemData } from '../../../domain/repositories/review-read.dto'; import { type ReviewItemData } from '../../../domain/repositories/review-read.dto';
import { REVIEW_REPOSITORY, type IReviewRepository, type PaginatedResult } from '../../../domain/repositories/review.repository'; import { REVIEW_REPOSITORY, type IReviewRepository, type PaginatedResult } from '../../../domain/repositories/review.repository';
import { GetReviewsByUserQuery } from './get-reviews-by-user.query'; 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<GetReviewsByUserQuery> { export class GetReviewsByUserHandler implements IQueryHandler<GetReviewsByUserQuery> {
constructor( constructor(
@Inject(REVIEW_REPOSITORY) private readonly reviewRepo: IReviewRepository, @Inject(REVIEW_REPOSITORY) private readonly reviewRepo: IReviewRepository,
private readonly logger: LoggerService,
) {} ) {}
async execute(query: GetReviewsByUserQuery): Promise<PaginatedResult<ReviewItemData>> { async execute(query: GetReviewsByUserQuery): Promise<PaginatedResult<ReviewItemData>> {
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á');
}
} }
} }