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:
@@ -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<AdjustSubscrip
|
||||
@Inject(SUBSCRIPTION_REPOSITORY) private readonly subscriptionRepo: ISubscriptionRepository,
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly eventBus: EventBus,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async execute(command: AdjustSubscriptionCommand): Promise<AdjustSubscriptionResult> {
|
||||
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}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ApproveKycCommand> {
|
||||
constructor(
|
||||
@Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository,
|
||||
private readonly eventBus: EventBus,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async execute(command: ApproveKycCommand): Promise<ApproveKycResult> {
|
||||
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',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ApproveListingComm
|
||||
constructor(
|
||||
@Inject(LISTING_REPOSITORY) private readonly listingRepo: IListingRepository,
|
||||
private readonly eventBus: EventBus,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async execute(command: ApproveListingCommand): Promise<ApproveListingResult> {
|
||||
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',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<BanUserCommand> {
|
||||
constructor(
|
||||
@Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository,
|
||||
private readonly eventBus: EventBus,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async execute(command: BanUserCommand): Promise<BanUserResult> {
|
||||
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',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<BulkModerate
|
||||
constructor(
|
||||
@Inject(LISTING_REPOSITORY) private readonly listingRepo: IListingRepository,
|
||||
private readonly eventBus: EventBus,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async execute(command: BulkModerateListingsCommand): Promise<BulkModerateResult> {
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<RejectKycCommand> {
|
||||
constructor(
|
||||
@Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository,
|
||||
private readonly eventBus: EventBus,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async execute(command: RejectKycCommand): Promise<RejectKycResult> {
|
||||
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',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<RejectListingComman
|
||||
constructor(
|
||||
@Inject(LISTING_REPOSITORY) private readonly listingRepo: IListingRepository,
|
||||
private readonly eventBus: EventBus,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async execute(command: RejectListingCommand): Promise<RejectListingResult> {
|
||||
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',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<UpdateUserStatus
|
||||
constructor(
|
||||
@Inject(USER_REPOSITORY) private readonly userRepo: IUserRepository,
|
||||
private readonly eventBus: EventBus,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async execute(command: UpdateUserStatusCommand): Promise<UpdateUserStatusResult> {
|
||||
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',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<GetAuditLogsQuery> {
|
||||
constructor(
|
||||
@Inject(AUDIT_LOG_REPOSITORY) private readonly auditRepo: IAuditLogRepository,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async execute(query: GetAuditLogsQuery): Promise<AuditLogListResult> {
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<GetDashboardStatsQuery> {
|
||||
constructor(
|
||||
@Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<GetKycQueueQuery> {
|
||||
constructor(
|
||||
@Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<GetModerationQueueQuery> {
|
||||
constructor(
|
||||
@Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<GetRevenueStatsQuery> {
|
||||
constructor(
|
||||
@Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<GetUserDetailQuery> {
|
||||
constructor(
|
||||
@Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async execute(query: GetUserDetailQuery): Promise<UserDetail> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<GetUsersQuery> {
|
||||
constructor(
|
||||
@Inject(ADMIN_QUERY_REPOSITORY) private readonly adminQueryRepo: IAdminQueryRepository,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async execute(query: GetUsersQuery): Promise<UserListResult> {
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||
import { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
|
||||
import { createId } from '@paralleldrive/cuid2';
|
||||
import { NotFoundException, type PrismaService, type LoggerService } from '@modules/shared';
|
||||
import { DomainException, NotFoundException, type PrismaService, type LoggerService } from '@modules/shared';
|
||||
import { InquiryEntity } from '../../../domain/entities/inquiry.entity';
|
||||
import { INQUIRY_REPOSITORY, type IInquiryRepository } from '../../../domain/repositories/inquiry.repository';
|
||||
import { CreateInquiryCommand } from './create-inquiry.command';
|
||||
@@ -22,38 +22,48 @@ export class CreateInquiryHandler implements ICommandHandler<CreateInquiryComman
|
||||
) {}
|
||||
|
||||
async execute(command: CreateInquiryCommand): Promise<CreateInquiryResult> {
|
||||
// Validate listing exists
|
||||
const listing = await this.prisma.listing.findUnique({
|
||||
where: { id: command.listingId },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!listing) {
|
||||
throw new NotFoundException('Listing', command.listingId);
|
||||
try {
|
||||
// Validate listing exists
|
||||
const listing = await this.prisma.listing.findUnique({
|
||||
where: { id: command.listingId },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!listing) {
|
||||
throw new NotFoundException('Listing', command.listingId);
|
||||
}
|
||||
|
||||
const id = createId();
|
||||
const inquiry = InquiryEntity.createNew(
|
||||
id,
|
||||
command.listingId,
|
||||
command.userId,
|
||||
command.message,
|
||||
command.phone,
|
||||
);
|
||||
|
||||
await this.inquiryRepo.save(inquiry);
|
||||
|
||||
// Publish domain events
|
||||
const events = inquiry.clearDomainEvents();
|
||||
for (const event of events) {
|
||||
this.eventBus.publish(event);
|
||||
}
|
||||
|
||||
this.logger.log(`Inquiry ${id} created by user ${command.userId} for listing ${command.listingId}`, 'CreateInquiryHandler');
|
||||
|
||||
return {
|
||||
id,
|
||||
listingId: command.listingId,
|
||||
createdAt: inquiry.createdAt.toISOString(),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof DomainException) throw error;
|
||||
this.logger.error(
|
||||
`Failed to create inquiry: ${error instanceof Error ? error.message : String(error)}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
'CreateInquiryHandler',
|
||||
);
|
||||
throw new InternalServerErrorException('Lỗi khi tạo yêu cầu tư vấn');
|
||||
}
|
||||
|
||||
const id = createId();
|
||||
const inquiry = InquiryEntity.createNew(
|
||||
id,
|
||||
command.listingId,
|
||||
command.userId,
|
||||
command.message,
|
||||
command.phone,
|
||||
);
|
||||
|
||||
await this.inquiryRepo.save(inquiry);
|
||||
|
||||
// Publish domain events
|
||||
const events = inquiry.clearDomainEvents();
|
||||
for (const event of events) {
|
||||
this.eventBus.publish(event);
|
||||
}
|
||||
|
||||
this.logger.log(`Inquiry ${id} created by user ${command.userId} for listing ${command.listingId}`, 'CreateInquiryHandler');
|
||||
|
||||
return {
|
||||
id,
|
||||
listingId: command.listingId,
|
||||
createdAt: inquiry.createdAt.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||
import { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
|
||||
import { ForbiddenException, NotFoundException, type PrismaService, type LoggerService } from '@modules/shared';
|
||||
import { DomainException, ForbiddenException, NotFoundException, type PrismaService, type LoggerService } from '@modules/shared';
|
||||
import { INQUIRY_REPOSITORY, type IInquiryRepository } from '../../../domain/repositories/inquiry.repository';
|
||||
import { MarkInquiryReadCommand } from './mark-inquiry-read.command';
|
||||
|
||||
@@ -14,37 +14,47 @@ export class MarkInquiryReadHandler implements ICommandHandler<MarkInquiryReadCo
|
||||
) {}
|
||||
|
||||
async execute(command: MarkInquiryReadCommand): Promise<void> {
|
||||
const inquiry = await this.inquiryRepo.findById(command.inquiryId);
|
||||
if (!inquiry) {
|
||||
throw new NotFoundException('Inquiry', command.inquiryId);
|
||||
try {
|
||||
const inquiry = await this.inquiryRepo.findById(command.inquiryId);
|
||||
if (!inquiry) {
|
||||
throw new NotFoundException('Inquiry', command.inquiryId);
|
||||
}
|
||||
|
||||
// Verify the requesting user is the listing's agent
|
||||
const listing = await this.prisma.listing.findUnique({
|
||||
where: { id: inquiry.listingId },
|
||||
select: { agentId: true },
|
||||
});
|
||||
if (!listing) {
|
||||
throw new NotFoundException('Listing', inquiry.listingId);
|
||||
}
|
||||
|
||||
const agent = await this.prisma.agent.findUnique({
|
||||
where: { userId: command.agentUserId },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!agent || listing.agentId !== agent.id) {
|
||||
throw new ForbiddenException('Bạn không có quyền đánh dấu yêu cầu tư vấn này');
|
||||
}
|
||||
|
||||
inquiry.markAsRead();
|
||||
await this.inquiryRepo.markAsRead(command.inquiryId);
|
||||
|
||||
// Publish domain events
|
||||
const events = inquiry.clearDomainEvents();
|
||||
for (const event of events) {
|
||||
this.eventBus.publish(event);
|
||||
}
|
||||
|
||||
this.logger.log(`Inquiry ${command.inquiryId} marked as read by agent ${command.agentUserId}`, 'MarkInquiryReadHandler');
|
||||
} catch (error) {
|
||||
if (error instanceof DomainException) throw error;
|
||||
this.logger.error(
|
||||
`Failed to mark inquiry as read: ${error instanceof Error ? error.message : String(error)}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
'MarkInquiryReadHandler',
|
||||
);
|
||||
throw new InternalServerErrorException('Lỗi khi đánh dấu đã đọc');
|
||||
}
|
||||
|
||||
// Verify the requesting user is the listing's agent
|
||||
const listing = await this.prisma.listing.findUnique({
|
||||
where: { id: inquiry.listingId },
|
||||
select: { agentId: true },
|
||||
});
|
||||
if (!listing) {
|
||||
throw new NotFoundException('Listing', inquiry.listingId);
|
||||
}
|
||||
|
||||
const agent = await this.prisma.agent.findUnique({
|
||||
where: { userId: command.agentUserId },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!agent || listing.agentId !== agent.id) {
|
||||
throw new ForbiddenException('Bạn không có quyền đánh dấu yêu cầu tư vấn này');
|
||||
}
|
||||
|
||||
inquiry.markAsRead();
|
||||
await this.inquiryRepo.markAsRead(command.inquiryId);
|
||||
|
||||
// Publish domain events
|
||||
const events = inquiry.clearDomainEvents();
|
||||
for (const event of events) {
|
||||
this.eventBus.publish(event);
|
||||
}
|
||||
|
||||
this.logger.log(`Inquiry ${command.inquiryId} marked as read by agent ${command.agentUserId}`, 'MarkInquiryReadHandler');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||
import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { NotFoundException, type PrismaService } from '@modules/shared';
|
||||
import { DomainException, NotFoundException, type PrismaService, type LoggerService } from '@modules/shared';
|
||||
import { type InquiryReadDto } from '../../../domain/repositories/inquiry-read.dto';
|
||||
import { INQUIRY_REPOSITORY, type IInquiryRepository, type PaginatedResult } from '../../../domain/repositories/inquiry.repository';
|
||||
import { GetInquiriesByAgentQuery } from './get-inquiries-by-agent.query';
|
||||
@@ -10,21 +10,32 @@ export class GetInquiriesByAgentHandler implements IQueryHandler<GetInquiriesByA
|
||||
constructor(
|
||||
@Inject(INQUIRY_REPOSITORY) private readonly inquiryRepo: IInquiryRepository,
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async execute(query: GetInquiriesByAgentQuery): Promise<PaginatedResult<InquiryReadDto>> {
|
||||
const agent = await this.prisma.agent.findUnique({
|
||||
where: { userId: query.agentUserId },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!agent) {
|
||||
throw new NotFoundException('Agent', query.agentUserId);
|
||||
}
|
||||
try {
|
||||
const agent = await this.prisma.agent.findUnique({
|
||||
where: { userId: query.agentUserId },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!agent) {
|
||||
throw new NotFoundException('Agent', query.agentUserId);
|
||||
}
|
||||
|
||||
return this.inquiryRepo.findByAgent(
|
||||
agent.id,
|
||||
query.page,
|
||||
query.limit,
|
||||
);
|
||||
return this.inquiryRepo.findByAgent(
|
||||
agent.id,
|
||||
query.page,
|
||||
query.limit,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof DomainException) throw error;
|
||||
this.logger.error(
|
||||
`Failed to get inquiries by agent: ${error instanceof Error ? error.message : String(error)}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
'GetInquiriesByAgentHandler',
|
||||
);
|
||||
throw new InternalServerErrorException('Lỗi khi lấy danh sách yêu cầu tư vấn');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||
import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { DomainException, type LoggerService } from '@modules/shared';
|
||||
import { type InquiryReadDto } from '../../../domain/repositories/inquiry-read.dto';
|
||||
import { INQUIRY_REPOSITORY, type IInquiryRepository, type PaginatedResult } from '../../../domain/repositories/inquiry.repository';
|
||||
import { GetInquiriesByListingQuery } from './get-inquiries-by-listing.query';
|
||||
@@ -8,13 +9,24 @@ import { GetInquiriesByListingQuery } from './get-inquiries-by-listing.query';
|
||||
export class GetInquiriesByListingHandler implements IQueryHandler<GetInquiriesByListingQuery> {
|
||||
constructor(
|
||||
@Inject(INQUIRY_REPOSITORY) private readonly inquiryRepo: IInquiryRepository,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async execute(query: GetInquiriesByListingQuery): Promise<PaginatedResult<InquiryReadDto>> {
|
||||
return this.inquiryRepo.findByListing(
|
||||
query.listingId,
|
||||
query.page,
|
||||
query.limit,
|
||||
);
|
||||
try {
|
||||
return await this.inquiryRepo.findByListing(
|
||||
query.listingId,
|
||||
query.page,
|
||||
query.limit,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof DomainException) throw error;
|
||||
this.logger.error(
|
||||
`Failed to get inquiries by listing: ${error instanceof Error ? error.message : String(error)}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
'GetInquiriesByListingHandler',
|
||||
);
|
||||
throw new InternalServerErrorException('Lỗi khi lấy danh sách yêu cầu tư vấn');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||
import { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
|
||||
import { createId } from '@paralleldrive/cuid2';
|
||||
import { NotFoundException, ValidationException, type PrismaService, type LoggerService } from '@modules/shared';
|
||||
import { DomainException, NotFoundException, ValidationException, type PrismaService, type LoggerService } from '@modules/shared';
|
||||
import { LeadEntity } from '../../../domain/entities/lead.entity';
|
||||
import { LEAD_REPOSITORY, type ILeadRepository } from '../../../domain/repositories/lead.repository';
|
||||
import { LeadScore } from '../../../domain/value-objects/lead-score.vo';
|
||||
@@ -23,50 +23,60 @@ export class CreateLeadHandler implements ICommandHandler<CreateLeadCommand> {
|
||||
) {}
|
||||
|
||||
async execute(command: CreateLeadCommand): Promise<CreateLeadResult> {
|
||||
// Look up agent by userId
|
||||
const agent = await this.prisma.agent.findUnique({
|
||||
where: { userId: command.agentUserId },
|
||||
});
|
||||
if (!agent) {
|
||||
throw new NotFoundException('Agent', command.agentUserId);
|
||||
}
|
||||
|
||||
// Validate score value object
|
||||
let score: LeadScore | null = null;
|
||||
if (command.score !== null && command.score !== undefined) {
|
||||
const scoreResult = LeadScore.create(command.score);
|
||||
if (scoreResult.isErr) {
|
||||
throw new ValidationException(scoreResult.unwrapErr());
|
||||
try {
|
||||
// Look up agent by userId
|
||||
const agent = await this.prisma.agent.findUnique({
|
||||
where: { userId: command.agentUserId },
|
||||
});
|
||||
if (!agent) {
|
||||
throw new NotFoundException('Agent', command.agentUserId);
|
||||
}
|
||||
score = scoreResult.unwrap();
|
||||
|
||||
// Validate score value object
|
||||
let score: LeadScore | null = null;
|
||||
if (command.score !== null && command.score !== undefined) {
|
||||
const scoreResult = LeadScore.create(command.score);
|
||||
if (scoreResult.isErr) {
|
||||
throw new ValidationException(scoreResult.unwrapErr());
|
||||
}
|
||||
score = scoreResult.unwrap();
|
||||
}
|
||||
|
||||
const id = createId();
|
||||
const lead = LeadEntity.createNew(
|
||||
id,
|
||||
agent.id,
|
||||
command.name,
|
||||
command.phone,
|
||||
command.email,
|
||||
command.source,
|
||||
score,
|
||||
command.notes ?? null,
|
||||
);
|
||||
|
||||
await this.leadRepo.save(lead);
|
||||
|
||||
// Publish domain events
|
||||
const events = lead.clearDomainEvents();
|
||||
for (const event of events) {
|
||||
this.eventBus.publish(event);
|
||||
}
|
||||
|
||||
this.logger.log(`Lead ${id} created by agent ${agent.id}`, 'CreateLeadHandler');
|
||||
|
||||
return {
|
||||
id,
|
||||
status: lead.status,
|
||||
createdAt: lead.createdAt.toISOString(),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof DomainException) throw error;
|
||||
this.logger.error(
|
||||
`Failed to create lead: ${error instanceof Error ? error.message : String(error)}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
'CreateLeadHandler',
|
||||
);
|
||||
throw new InternalServerErrorException('Lỗi khi tạo lead');
|
||||
}
|
||||
|
||||
const id = createId();
|
||||
const lead = LeadEntity.createNew(
|
||||
id,
|
||||
agent.id,
|
||||
command.name,
|
||||
command.phone,
|
||||
command.email,
|
||||
command.source,
|
||||
score,
|
||||
command.notes ?? null,
|
||||
);
|
||||
|
||||
await this.leadRepo.save(lead);
|
||||
|
||||
// Publish domain events
|
||||
const events = lead.clearDomainEvents();
|
||||
for (const event of events) {
|
||||
this.eventBus.publish(event);
|
||||
}
|
||||
|
||||
this.logger.log(`Lead ${id} created by agent ${agent.id}`, 'CreateLeadHandler');
|
||||
|
||||
return {
|
||||
id,
|
||||
status: lead.status,
|
||||
createdAt: lead.createdAt.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||
import { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
|
||||
import { ForbiddenException, NotFoundException, type PrismaService, type LoggerService } from '@modules/shared';
|
||||
import { DomainException, ForbiddenException, NotFoundException, type PrismaService, type LoggerService } from '@modules/shared';
|
||||
import { LEAD_REPOSITORY, type ILeadRepository } from '../../../domain/repositories/lead.repository';
|
||||
import { DeleteLeadCommand } from './delete-lead.command';
|
||||
|
||||
@@ -14,26 +14,36 @@ export class DeleteLeadHandler implements ICommandHandler<DeleteLeadCommand> {
|
||||
) {}
|
||||
|
||||
async execute(command: DeleteLeadCommand): Promise<void> {
|
||||
// Look up agent by userId
|
||||
const agent = await this.prisma.agent.findUnique({
|
||||
where: { userId: command.agentUserId },
|
||||
});
|
||||
if (!agent) {
|
||||
throw new NotFoundException('Agent', command.agentUserId);
|
||||
try {
|
||||
// Look up agent by userId
|
||||
const agent = await this.prisma.agent.findUnique({
|
||||
where: { userId: command.agentUserId },
|
||||
});
|
||||
if (!agent) {
|
||||
throw new NotFoundException('Agent', command.agentUserId);
|
||||
}
|
||||
|
||||
const lead = await this.leadRepo.findById(command.leadId);
|
||||
if (!lead) {
|
||||
throw new NotFoundException('Lead', command.leadId);
|
||||
}
|
||||
|
||||
// Verify agent ownership
|
||||
if (lead.agentId !== agent.id) {
|
||||
throw new ForbiddenException('Bạn chỉ có thể xóa lead của chính mình');
|
||||
}
|
||||
|
||||
await this.leadRepo.delete(command.leadId);
|
||||
|
||||
this.logger.log(`Lead ${command.leadId} deleted by agent ${agent.id}`, 'DeleteLeadHandler');
|
||||
} catch (error) {
|
||||
if (error instanceof DomainException) throw error;
|
||||
this.logger.error(
|
||||
`Failed to delete lead: ${error instanceof Error ? error.message : String(error)}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
'DeleteLeadHandler',
|
||||
);
|
||||
throw new InternalServerErrorException('Lỗi khi xóa lead');
|
||||
}
|
||||
|
||||
const lead = await this.leadRepo.findById(command.leadId);
|
||||
if (!lead) {
|
||||
throw new NotFoundException('Lead', command.leadId);
|
||||
}
|
||||
|
||||
// Verify agent ownership
|
||||
if (lead.agentId !== agent.id) {
|
||||
throw new ForbiddenException('Bạn chỉ có thể xóa lead của chính mình');
|
||||
}
|
||||
|
||||
await this.leadRepo.delete(command.leadId);
|
||||
|
||||
this.logger.log(`Lead ${command.leadId} deleted by agent ${agent.id}`, 'DeleteLeadHandler');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||
import { CommandHandler, type EventBus, type ICommandHandler } from '@nestjs/cqrs';
|
||||
import { ForbiddenException, NotFoundException, type PrismaService, type LoggerService } from '@modules/shared';
|
||||
import { DomainException, ForbiddenException, NotFoundException, type PrismaService, type LoggerService } from '@modules/shared';
|
||||
import { type LeadStatus } from '../../../domain/entities/lead.entity';
|
||||
import { LEAD_REPOSITORY, type ILeadRepository } from '../../../domain/repositories/lead.repository';
|
||||
import { UpdateLeadStatusCommand } from './update-lead-status.command';
|
||||
@@ -15,33 +15,43 @@ export class UpdateLeadStatusHandler implements ICommandHandler<UpdateLeadStatus
|
||||
) {}
|
||||
|
||||
async execute(command: UpdateLeadStatusCommand): Promise<void> {
|
||||
// Look up agent by userId
|
||||
const agent = await this.prisma.agent.findUnique({
|
||||
where: { userId: command.agentUserId },
|
||||
});
|
||||
if (!agent) {
|
||||
throw new NotFoundException('Agent', command.agentUserId);
|
||||
try {
|
||||
// Look up agent by userId
|
||||
const agent = await this.prisma.agent.findUnique({
|
||||
where: { userId: command.agentUserId },
|
||||
});
|
||||
if (!agent) {
|
||||
throw new NotFoundException('Agent', command.agentUserId);
|
||||
}
|
||||
|
||||
const lead = await this.leadRepo.findById(command.leadId);
|
||||
if (!lead) {
|
||||
throw new NotFoundException('Lead', command.leadId);
|
||||
}
|
||||
|
||||
// Verify agent ownership
|
||||
if (lead.agentId !== agent.id) {
|
||||
throw new ForbiddenException('Bạn chỉ có thể cập nhật lead của chính mình');
|
||||
}
|
||||
|
||||
lead.updateStatus(command.newStatus as LeadStatus);
|
||||
await this.leadRepo.update(lead);
|
||||
|
||||
// Publish domain events
|
||||
const events = lead.clearDomainEvents();
|
||||
for (const event of events) {
|
||||
this.eventBus.publish(event);
|
||||
}
|
||||
|
||||
this.logger.log(`Lead ${command.leadId} status updated to ${command.newStatus} by agent ${agent.id}`, 'UpdateLeadStatusHandler');
|
||||
} catch (error) {
|
||||
if (error instanceof DomainException) throw error;
|
||||
this.logger.error(
|
||||
`Failed to update lead status: ${error instanceof Error ? error.message : String(error)}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
'UpdateLeadStatusHandler',
|
||||
);
|
||||
throw new InternalServerErrorException('Lỗi khi cập nhật trạng thái lead');
|
||||
}
|
||||
|
||||
const lead = await this.leadRepo.findById(command.leadId);
|
||||
if (!lead) {
|
||||
throw new NotFoundException('Lead', command.leadId);
|
||||
}
|
||||
|
||||
// Verify agent ownership
|
||||
if (lead.agentId !== agent.id) {
|
||||
throw new ForbiddenException('Bạn chỉ có thể cập nhật lead của chính mình');
|
||||
}
|
||||
|
||||
lead.updateStatus(command.newStatus as LeadStatus);
|
||||
await this.leadRepo.update(lead);
|
||||
|
||||
// Publish domain events
|
||||
const events = lead.clearDomainEvents();
|
||||
for (const event of events) {
|
||||
this.eventBus.publish(event);
|
||||
}
|
||||
|
||||
this.logger.log(`Lead ${command.leadId} status updated to ${command.newStatus} by agent ${agent.id}`, 'UpdateLeadStatusHandler');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||
import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { NotFoundException, type PrismaService } from '@modules/shared';
|
||||
import { DomainException, NotFoundException, type PrismaService, type LoggerService } from '@modules/shared';
|
||||
import { LEAD_REPOSITORY, type ILeadRepository, type LeadStatsData } from '../../../domain/repositories/lead.repository';
|
||||
import { GetLeadStatsQuery } from './get-lead-stats.query';
|
||||
|
||||
@@ -9,17 +9,28 @@ export class GetLeadStatsHandler implements IQueryHandler<GetLeadStatsQuery> {
|
||||
constructor(
|
||||
@Inject(LEAD_REPOSITORY) private readonly leadRepo: ILeadRepository,
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async execute(query: GetLeadStatsQuery): Promise<LeadStatsData> {
|
||||
// Look up agent by userId
|
||||
const agent = await this.prisma.agent.findUnique({
|
||||
where: { userId: query.agentUserId },
|
||||
});
|
||||
if (!agent) {
|
||||
throw new NotFoundException('Agent', query.agentUserId);
|
||||
}
|
||||
try {
|
||||
// Look up agent by userId
|
||||
const agent = await this.prisma.agent.findUnique({
|
||||
where: { userId: query.agentUserId },
|
||||
});
|
||||
if (!agent) {
|
||||
throw new NotFoundException('Agent', query.agentUserId);
|
||||
}
|
||||
|
||||
return this.leadRepo.getStatsByAgent(agent.id);
|
||||
return this.leadRepo.getStatsByAgent(agent.id);
|
||||
} catch (error) {
|
||||
if (error instanceof DomainException) throw error;
|
||||
this.logger.error(
|
||||
`Failed to get lead stats: ${error instanceof Error ? error.message : String(error)}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
'GetLeadStatsHandler',
|
||||
);
|
||||
throw new InternalServerErrorException('Lỗi khi lấy thống kê lead');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { Inject, InternalServerErrorException } from '@nestjs/common';
|
||||
import { type IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||
import { NotFoundException, type PrismaService } from '@modules/shared';
|
||||
import { DomainException, NotFoundException, type PrismaService, type LoggerService } from '@modules/shared';
|
||||
import { type LeadReadDto } from '../../../domain/repositories/lead-read.dto';
|
||||
import { LEAD_REPOSITORY, type ILeadRepository, type PaginatedResult } from '../../../domain/repositories/lead.repository';
|
||||
import { GetLeadsByAgentQuery } from './get-leads-by-agent.query';
|
||||
@@ -10,22 +10,33 @@ export class GetLeadsByAgentHandler implements IQueryHandler<GetLeadsByAgentQuer
|
||||
constructor(
|
||||
@Inject(LEAD_REPOSITORY) private readonly leadRepo: ILeadRepository,
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async execute(query: GetLeadsByAgentQuery): Promise<PaginatedResult<LeadReadDto>> {
|
||||
// Look up agent by userId
|
||||
const agent = await this.prisma.agent.findUnique({
|
||||
where: { userId: query.agentUserId },
|
||||
});
|
||||
if (!agent) {
|
||||
throw new NotFoundException('Agent', query.agentUserId);
|
||||
}
|
||||
try {
|
||||
// Look up agent by userId
|
||||
const agent = await this.prisma.agent.findUnique({
|
||||
where: { userId: query.agentUserId },
|
||||
});
|
||||
if (!agent) {
|
||||
throw new NotFoundException('Agent', query.agentUserId);
|
||||
}
|
||||
|
||||
return this.leadRepo.findByAgent(
|
||||
agent.id,
|
||||
query.status,
|
||||
query.page,
|
||||
query.limit,
|
||||
);
|
||||
return this.leadRepo.findByAgent(
|
||||
agent.id,
|
||||
query.status,
|
||||
query.page,
|
||||
query.limit,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof DomainException) throw error;
|
||||
this.logger.error(
|
||||
`Failed to get leads by agent: ${error instanceof Error ? error.message : String(error)}`,
|
||||
error instanceof Error ? error.stack : undefined,
|
||||
'GetLeadsByAgentHandler',
|
||||
);
|
||||
throw new InternalServerErrorException('Lỗi khi lấy danh sách lead');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<CreateReviewCommand>
|
||||
) {}
|
||||
|
||||
async execute(command: CreateReviewCommand): Promise<CreateReviewResult> {
|
||||
// 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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<DeleteReviewCommand>
|
||||
) {}
|
||||
|
||||
async execute(command: DeleteReviewCommand): Promise<void> {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<GetAverageRatingQuery> {
|
||||
constructor(
|
||||
@Inject(REVIEW_REPOSITORY) private readonly reviewRepo: IReviewRepository,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<GetReviewsByTargetQuery> {
|
||||
constructor(
|
||||
@Inject(REVIEW_REPOSITORY) private readonly reviewRepo: IReviewRepository,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
async execute(query: GetReviewsByTargetQuery): Promise<PaginatedResult<ReviewItemData>> {
|
||||
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á');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<GetReviewsByUserQuery> {
|
||||
constructor(
|
||||
@Inject(REVIEW_REPOSITORY) private readonly reviewRepo: IReviewRepository,
|
||||
private readonly logger: LoggerService,
|
||||
) {}
|
||||
|
||||
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á');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user