chore: add comment reply for review and blog comment
This commit is contained in:
@@ -228,6 +228,15 @@ export const enum ServiceMessage {
|
||||
AUDIOS_SHOULD_BE_URL = "صوت های سرویس باید یک آدرس یو آر ال باشد",
|
||||
VIDEOS_REQUIRED = "ویدیو های سرویس مورد نیاز است",
|
||||
VIDEOS_SHOULD_BE_URL = "ویدیو های سرویس باید یک آدرس یو آر ال باشد",
|
||||
REVIEW_ID_SHOULD_BE_A_UUID = "شناسه نظر باید یک UUID باشد",
|
||||
REVIEW_ID_REQUIRED = "شناسه نظر مورد نیاز است",
|
||||
PARENT_REVIEW_ID_REQUIRED = "شناسه نظر والد مورد نیاز است",
|
||||
CONTENT_REQUIRED = "محتوا مورد نیاز است",
|
||||
CONTENT_STRING = "محتوا باید یک رشته باشد",
|
||||
PARENT_REVIEW_ID_SHOULD_BE_A_UUID = "شناسه نظر والد باید یک UUID باشد",
|
||||
CONTENT_LENGTH = "محتوا باید بین ۱ تا ۴۰۰ کاراکتر باشد",
|
||||
PARENT_REPLY_NOT_EXIST = "نظر والد مورد نظر یافت نشد",
|
||||
REVIEW_REPLY_ADDED = "نظر با موفقیت ثبت شد",
|
||||
}
|
||||
|
||||
export const enum AnnouncementMessage {
|
||||
@@ -803,6 +812,11 @@ export const enum BlogMessage {
|
||||
BLOG_ALREADY_EXISTS_WITH_THIS_SLUG = "بلاگی با این اسلاگ قبلا ثبت شده است",
|
||||
CREATED_AT_REQUIRED = "تاریخ ایجاد الزامی است",
|
||||
CREATED_AT_DATE_STRING = "تاریخ ایجاد باید یک تاریخ باشد",
|
||||
PARENT_REPLY_NOT_EXIST = "نظر والد مورد نظر یافت نشد",
|
||||
REPLY_NOT_FOUND = "نظر مورد نظر یافت نشد",
|
||||
PARENT_REPLY_ID_SHOULD_BE_A_UUID = "شناسه نظر والد باید یک UUID باشد",
|
||||
PARENT_REPLY_ID_REQUIRED = "شناسه نظر والد مورد نیاز است",
|
||||
REPLY_ADDED = "نظر با موفقیت ثبت شد",
|
||||
}
|
||||
|
||||
export const enum SliderMessage {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsOptional, IsString, IsUUID } from "class-validator";
|
||||
|
||||
import { BlogMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
export class CreateCommentReplyDto {
|
||||
@IsNotEmpty({ message: BlogMessage.CONTENT_REQUIRED })
|
||||
@IsString({ message: BlogMessage.CONTENT_STRING })
|
||||
@ApiProperty({ description: "the content of reply", example: "This is a reply" })
|
||||
content: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNotEmpty({ message: BlogMessage.PARENT_REPLY_ID_REQUIRED })
|
||||
@IsUUID("4", { message: BlogMessage.PARENT_REPLY_ID_SHOULD_BE_A_UUID })
|
||||
@ApiProperty({ description: "the parent reply id", example: "d290f1ee-6c54-4b01-90e6-d701748f0851" })
|
||||
parentReplyId?: string;
|
||||
}
|
||||
@@ -4,12 +4,14 @@ import { ApiOperation } from "@nestjs/swagger";
|
||||
import { BlogCommentQueryDto } from "./DTO/blog-comment-query.dto";
|
||||
import { CreateBlogDto } from "./DTO/create-blog.dto";
|
||||
import { CreateBlogCategoryDto } from "./DTO/create-category.dto";
|
||||
import { CreateCommentReplyDto } from "./DTO/create-comment-reply.dto";
|
||||
import { CreateCommentDto } from "./DTO/create-comment.dto";
|
||||
import { BlogSlugDto } from "./DTO/get-blog-by-slug.dto";
|
||||
import { BlogListSearchQueryDto, BlogSearchQueryDto } from "./DTO/search-blog-query.dto";
|
||||
import { UpdateBlogDto } from "./DTO/update-blog.dto";
|
||||
import { UpdateBlogCategoryDto } from "./DTO/update-category.dto";
|
||||
import { UpdateCommentStatusDto } from "./DTO/update-comment-status.dto";
|
||||
import { BlogCommentService } from "./providers/blog-comment.service";
|
||||
import { BlogsService } from "./providers/blogs.service";
|
||||
import { AdminRoute } from "../../common/decorators/admin.decorator";
|
||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||
@@ -23,7 +25,10 @@ import { PermissionEnum } from "../users/enums/permission.enum";
|
||||
@Controller("blogs")
|
||||
@AuthGuards()
|
||||
export class BlogsController {
|
||||
constructor(private readonly blogsService: BlogsService) {}
|
||||
constructor(
|
||||
private readonly blogsService: BlogsService,
|
||||
private readonly blogCommentService: BlogCommentService,
|
||||
) {}
|
||||
|
||||
@ApiOperation({ summary: "Get all public and active blogs with category id" })
|
||||
@SkipAuth()
|
||||
@@ -120,7 +125,13 @@ export class BlogsController {
|
||||
@ApiOperation({ summary: "Create a new comment" })
|
||||
@Post(":id/comments")
|
||||
createComment(@Param() paramDto: ParamDto, @Body() createCommentDto: CreateCommentDto, @UserDec("id") userId: string) {
|
||||
return this.blogsService.createComment(paramDto.id, createCommentDto, userId);
|
||||
return this.blogCommentService.createComment(paramDto.id, createCommentDto, userId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Add a reply to a comment" })
|
||||
@Post("comments/:id/replies")
|
||||
addCommentReply(@Param() paramDto: ParamDto, @Body() createCommentReplyDto: CreateCommentReplyDto, @UserDec("id") userId: string) {
|
||||
return this.blogCommentService.addCommentReply(paramDto.id, createCommentReplyDto, userId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Get all comments for a blog (admin route)" })
|
||||
@@ -128,7 +139,7 @@ export class BlogsController {
|
||||
@PermissionsDec(PermissionEnum.BLOGS)
|
||||
@Get(":id/comments")
|
||||
getBlogCommentsForAdmin(@Param() paramDto: ParamDto) {
|
||||
return this.blogsService.getBlogCommentsForAdmin(paramDto.id);
|
||||
return this.blogCommentService.getBlogCommentsForAdmin(paramDto.id);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Get all comments for a blog (admin route)" })
|
||||
@@ -136,7 +147,7 @@ export class BlogsController {
|
||||
@PermissionsDec(PermissionEnum.BLOGS)
|
||||
@Get("comments")
|
||||
getAllCommentForAdmin(@Query() queryDto: BlogCommentQueryDto) {
|
||||
return this.blogsService.getAllCommentForAdmin(queryDto);
|
||||
return this.blogCommentService.getAllCommentForAdmin(queryDto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Update the status of a comment (admin route)" })
|
||||
@@ -144,7 +155,7 @@ export class BlogsController {
|
||||
@PermissionsDec(PermissionEnum.BLOGS)
|
||||
@Patch("comments/:id/status")
|
||||
updateCommentStatus(@Param() paramDto: ParamDto, @Body() updateCommentStatusDto: UpdateCommentStatusDto) {
|
||||
return this.blogsService.updateCommentStatus(paramDto.id, updateCommentStatusDto);
|
||||
return this.blogCommentService.updateCommentStatus(paramDto.id, updateCommentStatusDto);
|
||||
}
|
||||
|
||||
//-------------- categories --------------
|
||||
|
||||
@@ -3,18 +3,29 @@ import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
|
||||
import { BlogsController } from "./blogs.controller";
|
||||
import { BlogCategory } from "./entities/blog-category.entity";
|
||||
import { BlogCommentReply } from "./entities/blog-comment-reply.entity";
|
||||
import { BlogComment } from "./entities/blog-comment.entity";
|
||||
import { Blog } from "./entities/blog.entity";
|
||||
import { BlogCommentService } from "./providers/blog-comment.service";
|
||||
import { BlogsService } from "./providers/blogs.service";
|
||||
import { BlogCategoriesRepository } from "./repositories/blog-categories.repository";
|
||||
import { BlogCommentReplyRepository } from "./repositories/blog-comment-reply.repository";
|
||||
import { BlogCommentsRepository } from "./repositories/blog-comments.repository";
|
||||
import { BlogsRepository } from "./repositories/blogs.repository";
|
||||
import { NotificationModule } from "../notifications/notifications.module";
|
||||
import { UsersModule } from "../users/users.module";
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Blog, BlogComment, BlogCategory]), UsersModule, NotificationModule],
|
||||
imports: [TypeOrmModule.forFeature([Blog, BlogComment, BlogCommentReply, BlogCategory]), UsersModule, NotificationModule],
|
||||
controllers: [BlogsController],
|
||||
providers: [BlogsService, BlogsRepository, BlogCommentsRepository, BlogCategoriesRepository],
|
||||
exports: [BlogsService],
|
||||
providers: [
|
||||
BlogsService,
|
||||
BlogCommentService,
|
||||
BlogsRepository,
|
||||
BlogCommentsRepository,
|
||||
BlogCommentReplyRepository,
|
||||
BlogCategoriesRepository,
|
||||
],
|
||||
exports: [BlogsService, BlogCommentService],
|
||||
})
|
||||
export class BlogsModule {}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Column, Entity, ManyToOne, OneToMany } from "typeorm";
|
||||
|
||||
import { BlogComment } from "./blog-comment.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
|
||||
@Entity()
|
||||
export class BlogCommentReply extends BaseEntity {
|
||||
@Column({ type: "text", nullable: false })
|
||||
content: string;
|
||||
|
||||
@Column({ type: "boolean", default: false })
|
||||
isOfficialReply: boolean;
|
||||
|
||||
@ManyToOne(() => BlogComment, (comment) => comment.replies, { onDelete: "CASCADE" })
|
||||
comment: BlogComment;
|
||||
|
||||
@ManyToOne(() => User, { onDelete: "SET NULL" })
|
||||
author: User;
|
||||
|
||||
@ManyToOne(() => BlogCommentReply, (reply) => reply.replies, { nullable: true })
|
||||
parentReply: BlogCommentReply | null;
|
||||
|
||||
@OneToMany(() => BlogCommentReply, (reply) => reply.parentReply)
|
||||
replies: BlogCommentReply[];
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Column, Entity, ManyToOne } from "typeorm";
|
||||
import { Column, Entity, ManyToOne, OneToMany } from "typeorm";
|
||||
|
||||
import { BlogCommentReply } from "./blog-comment-reply.entity";
|
||||
import { Blog } from "./blog.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
@@ -21,4 +22,7 @@ export class BlogComment extends BaseEntity {
|
||||
|
||||
@ManyToOne(() => Blog, (blog) => blog.comments, { onDelete: "CASCADE" })
|
||||
blog: Blog;
|
||||
|
||||
@OneToMany(() => BlogCommentReply, (reply) => reply.comment)
|
||||
replies: BlogCommentReply[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
import { DataSource, IsNull } from "typeorm";
|
||||
|
||||
import { BlogMessage, CommonMessage } from "../../../common/enums/message.enum";
|
||||
import { NotificationQueue } from "../../notifications/queue/notification.queue";
|
||||
import { AdminsService } from "../../users/providers/admins.service";
|
||||
import { UsersService } from "../../users/providers/users.service";
|
||||
import { BlogCommentQueryDto } from "../DTO/blog-comment-query.dto";
|
||||
import { CreateCommentReplyDto } from "../DTO/create-comment-reply.dto";
|
||||
import { CreateCommentDto } from "../DTO/create-comment.dto";
|
||||
import { UpdateCommentStatusDto } from "../DTO/update-comment-status.dto";
|
||||
import { BlogCommentReply } from "../entities/blog-comment-reply.entity";
|
||||
import { BlogComment } from "../entities/blog-comment.entity";
|
||||
import { CommentStatus } from "../enums/comment-status.enum";
|
||||
import { BlogCommentReplyRepository } from "../repositories/blog-comment-reply.repository";
|
||||
import { BlogCommentsRepository } from "../repositories/blog-comments.repository";
|
||||
import { BlogsRepository } from "../repositories/blogs.repository";
|
||||
|
||||
@Injectable()
|
||||
export class BlogCommentService {
|
||||
constructor(
|
||||
private blogsRepository: BlogsRepository,
|
||||
private blogCommentReplyRepository: BlogCommentReplyRepository,
|
||||
private blogCommentsRepository: BlogCommentsRepository,
|
||||
private usersService: UsersService,
|
||||
private dataSource: DataSource,
|
||||
private adminsService: AdminsService,
|
||||
private notificationQueue: NotificationQueue,
|
||||
) {}
|
||||
|
||||
//########################################################
|
||||
async addCommentReply(commentId: string, createReplyDto: CreateCommentReplyDto, authorId: string) {
|
||||
const comment = await this.blogCommentsRepository.findOne({ where: { id: commentId } });
|
||||
|
||||
let parentReply: BlogCommentReply | null = null;
|
||||
|
||||
if (!comment) throw new BadRequestException(BlogMessage.COMMENT_NOT_FOUND);
|
||||
|
||||
if (createReplyDto.parentReplyId) {
|
||||
parentReply = await this.blogCommentReplyRepository.findOne({ where: { id: createReplyDto.parentReplyId } });
|
||||
|
||||
if (!parentReply) throw new BadRequestException(BlogMessage.PARENT_REPLY_NOT_EXIST);
|
||||
}
|
||||
|
||||
const isSuperAdmin = await this.usersService.isSuperAdmin(authorId);
|
||||
|
||||
const reply = this.blogCommentReplyRepository.create({
|
||||
content: createReplyDto.content,
|
||||
isOfficialReply: isSuperAdmin || false,
|
||||
author: { id: authorId },
|
||||
comment: { id: commentId } as BlogComment,
|
||||
parentReply,
|
||||
});
|
||||
|
||||
await this.blogCommentReplyRepository.save(reply);
|
||||
|
||||
return {
|
||||
message: BlogMessage.REPLY_ADDED,
|
||||
reply,
|
||||
};
|
||||
}
|
||||
|
||||
//*********************************** */
|
||||
|
||||
async createComment(blogId: string, createCommentDto: CreateCommentDto, userId: string) {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
|
||||
try {
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
const blog = await queryRunner.manager.findOne(this.blogsRepository.target, { where: { id: blogId, deletedAt: IsNull() } });
|
||||
if (!blog) throw new BadRequestException(BlogMessage.BLOG_NOT_FOUND);
|
||||
|
||||
const user = await this.usersService.findOneByIdWithQueryRunner(userId, queryRunner);
|
||||
|
||||
const comment = queryRunner.manager.create(this.blogCommentsRepository.target, {
|
||||
...createCommentDto,
|
||||
blog,
|
||||
user,
|
||||
status: CommentStatus.APPROVED,
|
||||
});
|
||||
|
||||
await queryRunner.manager.save(this.blogCommentsRepository.target, comment);
|
||||
|
||||
const superAdmin = await this.adminsService.getSuperAdmins(queryRunner);
|
||||
|
||||
for (const admin of superAdmin) {
|
||||
await this.notificationQueue.addNewBlogCommentNotification(admin.id, {
|
||||
userPhone: admin.phone,
|
||||
userEmail: admin.email,
|
||||
blogTitle: blog.title,
|
||||
fullName: `${user.firstName} ${user.lastName}`,
|
||||
});
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
return {
|
||||
message: CommonMessage.CREATED,
|
||||
comment,
|
||||
};
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
//*********************************** */
|
||||
|
||||
async updateCommentStatus(commentId: string, updateCommentStatusDto: UpdateCommentStatusDto) {
|
||||
const comment = await this.blogCommentsRepository.findOneById(commentId);
|
||||
if (!comment) throw new BadRequestException(BlogMessage.COMMENT_NOT_FOUND);
|
||||
|
||||
comment.status = updateCommentStatusDto.status;
|
||||
await this.blogCommentsRepository.save(comment);
|
||||
|
||||
return {
|
||||
message: CommonMessage.UPDATE_SUCCESS,
|
||||
comment,
|
||||
};
|
||||
}
|
||||
//*********************************** */
|
||||
|
||||
async getBlogCommentsForAdmin(blogId: string) {
|
||||
const comments = await this.blogCommentsRepository.findBlogCommentsForAdmin(blogId);
|
||||
return { comments };
|
||||
}
|
||||
|
||||
//*********************************** */
|
||||
|
||||
async getAllCommentForAdmin(queryDto: BlogCommentQueryDto) {
|
||||
const [comments, count] = await this.blogCommentsRepository.getAllCommentsForAdmin(queryDto);
|
||||
return { comments, count, paginate: true };
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,14 @@
|
||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
import { DataSource, IsNull, Not } from "typeorm";
|
||||
import { Not } from "typeorm";
|
||||
|
||||
import { AdminMessage, BlogMessage, CommonMessage } from "../../../common/enums/message.enum";
|
||||
import { CategoryListSearchQueryDto } from "../../danak-services/DTO/category-search-query.dto";
|
||||
import { NotificationQueue } from "../../notifications/queue/notification.queue";
|
||||
import { AdminsService } from "../../users/providers/admins.service";
|
||||
import { UsersService } from "../../users/providers/users.service";
|
||||
import { BlogCommentQueryDto } from "../DTO/blog-comment-query.dto";
|
||||
import { CreateBlogDto } from "../DTO/create-blog.dto";
|
||||
import { CreateBlogCategoryDto } from "../DTO/create-category.dto";
|
||||
import { CreateCommentDto } from "../DTO/create-comment.dto";
|
||||
import { BlogListSearchQueryDto, BlogSearchQueryDto } from "../DTO/search-blog-query.dto";
|
||||
import { UpdateBlogDto } from "../DTO/update-blog.dto";
|
||||
import { UpdateBlogCategoryDto } from "../DTO/update-category.dto";
|
||||
import { UpdateCommentStatusDto } from "../DTO/update-comment-status.dto";
|
||||
import { CommentStatus } from "../enums/comment-status.enum";
|
||||
import { BlogCategoriesRepository } from "../repositories/blog-categories.repository";
|
||||
import { BlogCommentsRepository } from "../repositories/blog-comments.repository";
|
||||
import { BlogsRepository } from "../repositories/blogs.repository";
|
||||
@@ -25,9 +19,6 @@ export class BlogsService {
|
||||
private readonly blogCommentsRepository: BlogCommentsRepository,
|
||||
private readonly blogCategoriesRepository: BlogCategoriesRepository,
|
||||
private readonly usersService: UsersService,
|
||||
private readonly adminsService: AdminsService,
|
||||
private readonly dataSource: DataSource,
|
||||
private readonly notificationQueue: NotificationQueue,
|
||||
) {}
|
||||
|
||||
//*********************************** */
|
||||
@@ -235,80 +226,6 @@ export class BlogsService {
|
||||
const [blogs, count] = await this.blogsRepository.getBlogListForAdmin(queryDto);
|
||||
return { blogs, count, paginate: true };
|
||||
}
|
||||
//*********************************** */
|
||||
|
||||
async createComment(blogId: string, createCommentDto: CreateCommentDto, userId: string) {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
|
||||
try {
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
const blog = await queryRunner.manager.findOne(this.blogsRepository.target, { where: { id: blogId, deletedAt: IsNull() } });
|
||||
if (!blog) throw new BadRequestException(BlogMessage.BLOG_NOT_FOUND);
|
||||
|
||||
const user = await this.usersService.findOneByIdWithQueryRunner(userId, queryRunner);
|
||||
|
||||
const comment = queryRunner.manager.create(this.blogCommentsRepository.target, {
|
||||
...createCommentDto,
|
||||
blog,
|
||||
user,
|
||||
status: CommentStatus.PENDING,
|
||||
});
|
||||
|
||||
await queryRunner.manager.save(this.blogCommentsRepository.target, comment);
|
||||
|
||||
const superAdmin = await this.adminsService.getSuperAdmins(queryRunner);
|
||||
|
||||
for (const admin of superAdmin) {
|
||||
await this.notificationQueue.addNewBlogCommentNotification(admin.id, {
|
||||
userPhone: admin.phone,
|
||||
userEmail: admin.email,
|
||||
blogTitle: blog.title,
|
||||
fullName: `${user.firstName} ${user.lastName}`,
|
||||
});
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
return {
|
||||
message: CommonMessage.CREATED,
|
||||
comment,
|
||||
};
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
//*********************************** */
|
||||
|
||||
async updateCommentStatus(commentId: string, updateCommentStatusDto: UpdateCommentStatusDto) {
|
||||
const comment = await this.blogCommentsRepository.findOneById(commentId);
|
||||
if (!comment) throw new BadRequestException(BlogMessage.COMMENT_NOT_FOUND);
|
||||
|
||||
comment.status = updateCommentStatusDto.status;
|
||||
await this.blogCommentsRepository.save(comment);
|
||||
|
||||
return {
|
||||
message: CommonMessage.UPDATE_SUCCESS,
|
||||
comment,
|
||||
};
|
||||
}
|
||||
//*********************************** */
|
||||
|
||||
async getBlogCommentsForAdmin(blogId: string) {
|
||||
const comments = await this.blogCommentsRepository.findBlogCommentsForAdmin(blogId);
|
||||
return { comments };
|
||||
}
|
||||
|
||||
//*********************************** */
|
||||
|
||||
async getAllCommentForAdmin(queryDto: BlogCommentQueryDto) {
|
||||
const [comments, count] = await this.blogCommentsRepository.getAllCommentsForAdmin(queryDto);
|
||||
return { comments, count, paginate: true };
|
||||
}
|
||||
|
||||
//*********************************** */
|
||||
private generateSlug(slug: string): string {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
|
||||
import { BlogCommentReply } from "../entities/blog-comment-reply.entity";
|
||||
|
||||
@Injectable()
|
||||
export class BlogCommentReplyRepository extends Repository<BlogCommentReply> {
|
||||
constructor(@InjectRepository(BlogCommentReply) repository: Repository<BlogCommentReply>) {
|
||||
super(repository.target, repository.manager, repository.queryRunner);
|
||||
}
|
||||
//+********************
|
||||
async findByCommentId(commentId: string): Promise<BlogCommentReply[]> {
|
||||
return this.createQueryBuilder("reply")
|
||||
.leftJoinAndSelect("reply.author", "author")
|
||||
.leftJoinAndSelect("reply.parentReply", "parentReply")
|
||||
.leftJoinAndSelect("reply.replies", "replies")
|
||||
.where("reply.comment.id = :commentId", { commentId })
|
||||
.andWhere("reply.parentReply IS NULL")
|
||||
.orderBy("reply.createdAt", "ASC")
|
||||
.getMany();
|
||||
}
|
||||
//+********************
|
||||
async findRepliesByParentId(parentId: string): Promise<BlogCommentReply[]> {
|
||||
return this.createQueryBuilder("reply")
|
||||
.leftJoinAndSelect("reply.author", "author")
|
||||
.where("reply.parentReply.id = :parentId", { parentId })
|
||||
.orderBy("reply.createdAt", "ASC")
|
||||
.getMany();
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ export class BlogCommentsRepository extends Repository<BlogComment> {
|
||||
order: { createdAt: "DESC" },
|
||||
relations: {
|
||||
user: true,
|
||||
replies: true,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -33,6 +34,7 @@ export class BlogCommentsRepository extends Repository<BlogComment> {
|
||||
createdAt: true,
|
||||
status: true,
|
||||
user: { id: true, firstName: true, lastName: true },
|
||||
replies: { id: true, content: true, createdAt: true, author: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -44,7 +46,11 @@ export class BlogCommentsRepository extends Repository<BlogComment> {
|
||||
.where("blog.deletedAt IS NULL")
|
||||
.addSelect(["blog.id", "blog.title"])
|
||||
.leftJoin("blogComment.user", "user")
|
||||
.addSelect(["user.id", "user.firstName", "user.lastName"]);
|
||||
.addSelect(["user.id", "user.firstName", "user.lastName"])
|
||||
.leftJoin("blogComment.replies", "replies")
|
||||
.addSelect(["replies.id", "replies.content", "replies.createdAt"])
|
||||
.leftJoin("replies.author", "repliesAuthor")
|
||||
.addSelect(["repliesAuthor.id", "repliesAuthor.firstName", "repliesAuthor.lastName"]);
|
||||
|
||||
if (queryDto.q) {
|
||||
query.andWhere("blogComment.content ILIKE :q", { q: `%${queryDto.q}%` });
|
||||
@@ -58,13 +64,14 @@ export class BlogCommentsRepository extends Repository<BlogComment> {
|
||||
|
||||
return query.getManyAndCount();
|
||||
}
|
||||
|
||||
//*********************************** */
|
||||
async findBlogCommentsByBlogId(blogId: string) {
|
||||
return this.find({
|
||||
where: { blog: { id: blogId, deletedAt: IsNull() }, status: CommentStatus.APPROVED },
|
||||
order: { createdAt: "DESC" },
|
||||
relations: {
|
||||
user: true,
|
||||
replies: true,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
@@ -73,6 +80,7 @@ export class BlogCommentsRepository extends Repository<BlogComment> {
|
||||
createdAt: true,
|
||||
status: true,
|
||||
user: { id: true, firstName: true, lastName: true, profilePic: true },
|
||||
replies: { id: true, content: true, createdAt: true, author: { id: true, firstName: true, lastName: true } },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsOptional, IsString, IsUUID, Length } from "class-validator";
|
||||
|
||||
import { ServiceMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
export class AddReviewReplyDto {
|
||||
@IsNotEmpty({ message: ServiceMessage.CONTENT_REQUIRED })
|
||||
@Length(1, 400, { message: ServiceMessage.CONTENT_LENGTH })
|
||||
@IsString({ message: ServiceMessage.CONTENT_STRING })
|
||||
@ApiProperty({ description: "content of reply", example: "This is a reply" })
|
||||
content: string;
|
||||
|
||||
// @IsNotEmpty({ message: ServiceMessage.REVIEW_ID_REQUIRED })
|
||||
// @IsUUID("4", { message: ServiceMessage.REVIEW_ID_SHOULD_BE_A_UUID })
|
||||
// @ApiProperty({ description: "review id", example: "d290f1ee-6c54-4b01-90e6-d701748f0851" })
|
||||
// reviewId: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNotEmpty({ message: ServiceMessage.PARENT_REVIEW_ID_REQUIRED })
|
||||
@IsUUID("4", { message: ServiceMessage.PARENT_REVIEW_ID_SHOULD_BE_A_UUID })
|
||||
@ApiProperty({ description: "parent reply id", example: "d290f1ee-6c54-4b01-90e6-d701748f0851" })
|
||||
parentReplyId?: string;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Body, Controller, Delete, Get, Headers, HttpCode, HttpStatus, Ip, Param, Patch, Post, Query } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
|
||||
import { AddReviewReplyDto } from "./DTO/add-review-reply.dto";
|
||||
import { AddReviewDto } from "./DTO/add-review.dto";
|
||||
import { CategoryListSearchQueryDto, CategorySearchQueryDto } from "./DTO/category-search-query.dto";
|
||||
import { CreateDanakServiceCategoryDto } from "./DTO/create-category.dto";
|
||||
@@ -16,6 +17,7 @@ import { UpdateDanakServiceCategoryDto } from "./DTO/update-category.dto";
|
||||
import { UpdateReviewStatusDto } from "./DTO/update-review-status.dto";
|
||||
import { UpdateServiceGuideDto } from "./DTO/update-service-guide.dto";
|
||||
import { UpdateServiceDto } from "./DTO/update-service.dto";
|
||||
import { DanakServiceReviewService } from "./providers/danak-service-review.service";
|
||||
import { DanakServicesService } from "./providers/danak-services.service";
|
||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||
import { Pagination } from "../../common/decorators/pagination.decorator";
|
||||
@@ -28,7 +30,10 @@ import { PermissionEnum } from "../users/enums/permission.enum";
|
||||
@Controller("danak-services")
|
||||
@ApiTags("Danak-Services")
|
||||
export class DanakServicesController {
|
||||
constructor(private readonly danakServicesService: DanakServicesService) {}
|
||||
constructor(
|
||||
private readonly danakServicesService: DanakServicesService,
|
||||
private readonly danakServiceReviewService: DanakServiceReviewService,
|
||||
) {}
|
||||
|
||||
//------------------------ service categories ------------------------
|
||||
@AuthGuards()
|
||||
@@ -209,7 +214,14 @@ export class DanakServicesController {
|
||||
@AuthGuards()
|
||||
@Post(":id/reviews")
|
||||
addReview(@Param() paramDto: ParamDto, @Body() addReviewDto: AddReviewDto, @UserDec("id") userId: string) {
|
||||
return this.danakServicesService.addReview(userId, paramDto.id, addReviewDto);
|
||||
return this.danakServiceReviewService.addReview(userId, paramDto.id, addReviewDto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "add review reply to danak service review ==> user route" })
|
||||
@AuthGuards()
|
||||
@Post("reviews/:id/replies")
|
||||
addReviewReply(@Param() paramDto: ParamDto, @Body() addReviewReplyDto: AddReviewReplyDto, @UserDec("id") userId: string) {
|
||||
return this.danakServiceReviewService.addReviewReply(paramDto.id, addReviewReplyDto, userId);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "get danak service reviews ==> admin route" })
|
||||
@@ -218,7 +230,7 @@ export class DanakServicesController {
|
||||
@PermissionsDec(PermissionEnum.SERVICES)
|
||||
@Get("reviews")
|
||||
getDanakServiceReviews(@Query() queryDto: DanakServiceReviewQueryDto) {
|
||||
return this.danakServicesService.getDanakServiceReviews(queryDto);
|
||||
return this.danakServiceReviewService.getDanakServiceReviews(queryDto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "update danak service review status ==> admin route" })
|
||||
@@ -226,7 +238,7 @@ export class DanakServicesController {
|
||||
@PermissionsDec(PermissionEnum.SERVICES)
|
||||
@Patch("reviews/:id/:status")
|
||||
updateDanakServiceReviewStatus(@Param() updateParamDto: UpdateReviewStatusDto) {
|
||||
return this.danakServicesService.updateDanakServiceReviewStatus(updateParamDto);
|
||||
return this.danakServiceReviewService.updateDanakServiceReviewStatus(updateParamDto);
|
||||
}
|
||||
|
||||
//------------------------ service guides ------------------------
|
||||
|
||||
@@ -2,24 +2,27 @@ import { Module } from "@nestjs/common";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
|
||||
import { DanakServicesController } from "./danak-services.controller";
|
||||
import { DanakServiceAudio } from "./entities/danak-service-audio.entity";
|
||||
import { DanakServiceCategory } from "./entities/danak-service-category.entity";
|
||||
import { DanakServiceFeedback } from "./entities/danak-service-feedback.entity";
|
||||
import { DanakServiceGuide } from "./entities/danak-service-guide.entity";
|
||||
import { DanakServiceImage } from "./entities/danak-service-image.entity";
|
||||
import { DanakServiceReviewReply } from "./entities/danak-service-review-reply.entity";
|
||||
import { DanakServiceReview } from "./entities/danak-service-review.entity";
|
||||
import { DanakService } from "./entities/danak-service.entity";
|
||||
import { DanakServiceReviewService } from "./providers/danak-service-review.service";
|
||||
import { DanakServicesService } from "./providers/danak-services.service";
|
||||
import { DanakServiceFeedbackRepository } from "./repositories/danak-service-feedback.repository";
|
||||
import { DanakServiceGuideRepository } from "./repositories/danak-service-guide.repository";
|
||||
import { DanakServiceReviewReplyRepository } from "./repositories/danak-service-review-reply.repository";
|
||||
import { DanakServicesAudioRepository } from "./repositories/danak-services-audio.repository";
|
||||
import { DanakServicesCategoryRepository } from "./repositories/danak-services-category.repository";
|
||||
import { DanakServicesImageRepository } from "./repositories/danak-services-image.repository";
|
||||
import { DanakServiceReviewRepository } from "./repositories/danak-services-review.repository";
|
||||
import { DanakServicesRepository } from "./repositories/danak-services.repository";
|
||||
import { NotificationModule } from "../notifications/notifications.module";
|
||||
import { UsersModule } from "../users/users.module";
|
||||
import { DanakServiceAudio } from "./entities/danak-service-audio.entity";
|
||||
import { DanakServiceVideo } from "./entities/danak-service-video.entity";
|
||||
import { DanakServicesAudioRepository } from "./repositories/danak-services-audio.repository";
|
||||
import { DanakServicesVideoRepository } from "./repositories/danak-services-video.repository";
|
||||
|
||||
@Module({
|
||||
@@ -29,6 +32,7 @@ import { DanakServicesVideoRepository } from "./repositories/danak-services-vide
|
||||
DanakServiceImage,
|
||||
DanakServiceCategory,
|
||||
DanakServiceReview,
|
||||
DanakServiceReviewReply,
|
||||
DanakServiceFeedback,
|
||||
DanakServiceGuide,
|
||||
DanakServiceAudio,
|
||||
@@ -39,6 +43,7 @@ import { DanakServicesVideoRepository } from "./repositories/danak-services-vide
|
||||
],
|
||||
providers: [
|
||||
DanakServicesService,
|
||||
DanakServiceReviewService,
|
||||
DanakServicesRepository,
|
||||
DanakServicesCategoryRepository,
|
||||
DanakServicesImageRepository,
|
||||
@@ -47,8 +52,9 @@ import { DanakServicesVideoRepository } from "./repositories/danak-services-vide
|
||||
DanakServiceGuideRepository,
|
||||
DanakServicesAudioRepository,
|
||||
DanakServicesVideoRepository,
|
||||
DanakServiceReviewReplyRepository,
|
||||
],
|
||||
controllers: [DanakServicesController],
|
||||
exports: [DanakServicesService],
|
||||
exports: [DanakServicesService, DanakServiceReviewService],
|
||||
})
|
||||
export class DanakServicesModule {}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Column, Entity, ManyToOne, OneToMany } from "typeorm";
|
||||
|
||||
import { DanakServiceReview } from "./danak-service-review.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { ReviewStatus } from "../enums/review-status.enum";
|
||||
|
||||
@Entity()
|
||||
export class DanakServiceReviewReply extends BaseEntity {
|
||||
@Column({ type: "text", nullable: false })
|
||||
content: string;
|
||||
|
||||
@Column({ type: "boolean", default: false })
|
||||
isOfficialReply: boolean;
|
||||
|
||||
@ManyToOne(() => DanakServiceReview, (review) => review.replies, { onDelete: "CASCADE" })
|
||||
review: DanakServiceReview;
|
||||
|
||||
@ManyToOne(() => User, { onDelete: "SET NULL" })
|
||||
author: User;
|
||||
|
||||
@ManyToOne(() => DanakServiceReviewReply, (reply) => reply.replies, { nullable: true })
|
||||
parentReply: DanakServiceReviewReply | null;
|
||||
|
||||
@OneToMany(() => DanakServiceReviewReply, (reply) => reply.parentReply)
|
||||
replies: DanakServiceReviewReply[];
|
||||
|
||||
@Column({ type: "enum", enum: ReviewStatus, default: ReviewStatus.PENDING })
|
||||
status: ReviewStatus;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Column, Entity, ManyToOne } from "typeorm";
|
||||
import { Column, Entity, ManyToOne, OneToMany } from "typeorm";
|
||||
|
||||
import { DanakServiceReviewReply } from "./danak-service-review-reply.entity";
|
||||
import { DanakService } from "./danak-service.entity";
|
||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
@@ -24,4 +25,7 @@ export class DanakServiceReview extends BaseEntity {
|
||||
|
||||
@ManyToOne(() => DanakService, (service) => service.reviews, { onDelete: "CASCADE" })
|
||||
service: DanakService;
|
||||
|
||||
@OneToMany(() => DanakServiceReviewReply, (reply) => reply.review)
|
||||
replies: DanakServiceReviewReply[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
|
||||
import { DataSource, IsNull, QueryRunner } from "typeorm";
|
||||
|
||||
import { ServiceMessage, SubscriptionMessage } from "../../../common/enums/message.enum";
|
||||
import { NotificationQueue } from "../../notifications/queue/notification.queue";
|
||||
import { SubscriptionStatus } from "../../subscriptions/enums/subscription-status.enum";
|
||||
import { AdminsService } from "../../users/providers/admins.service";
|
||||
import { UsersService } from "../../users/providers/users.service";
|
||||
import { AddReviewReplyDto } from "../DTO/add-review-reply.dto";
|
||||
import { AddReviewDto } from "../DTO/add-review.dto";
|
||||
import { DanakServiceReviewQueryDto } from "../DTO/danak-service-review-query.dto";
|
||||
import { UpdateReviewStatusDto } from "../DTO/update-review-status.dto";
|
||||
import { DanakServiceReviewReply } from "../entities/danak-service-review-reply.entity";
|
||||
import { ReviewStatus } from "../enums/review-status.enum";
|
||||
import { DanakServiceReviewReplyRepository } from "../repositories/danak-service-review-reply.repository";
|
||||
import { DanakServiceReviewRepository } from "../repositories/danak-services-review.repository";
|
||||
import { DanakServicesRepository } from "../repositories/danak-services.repository";
|
||||
|
||||
@Injectable()
|
||||
export class DanakServiceReviewService {
|
||||
private readonly logger = new Logger(DanakServiceReviewService.name);
|
||||
|
||||
constructor(
|
||||
private danakServiceReviewRepository: DanakServiceReviewRepository,
|
||||
private replyRepository: DanakServiceReviewReplyRepository,
|
||||
private danakServicesRepository: DanakServicesRepository,
|
||||
private dataSource: DataSource,
|
||||
private usersService: UsersService,
|
||||
private adminsService: AdminsService,
|
||||
private notificationQueue: NotificationQueue,
|
||||
) {}
|
||||
|
||||
//########################################################
|
||||
async addReviewReply(reviewId: string, addReviewReplyDto: AddReviewReplyDto, authorId: string) {
|
||||
const review = await this.danakServiceReviewRepository.findOne({ where: { id: reviewId } });
|
||||
|
||||
let parentReply: DanakServiceReviewReply | null = null;
|
||||
|
||||
if (!review) throw new BadRequestException(ServiceMessage.REVIEW_NOT_EXIST);
|
||||
|
||||
if (addReviewReplyDto.parentReplyId) {
|
||||
parentReply = await this.replyRepository.findOne({ where: { id: addReviewReplyDto.parentReplyId } });
|
||||
|
||||
if (!parentReply) throw new BadRequestException(ServiceMessage.PARENT_REPLY_NOT_EXIST);
|
||||
}
|
||||
|
||||
const isSuperAdmin = await this.usersService.isSuperAdmin(authorId);
|
||||
|
||||
const reply = this.replyRepository.create({
|
||||
content: addReviewReplyDto.content,
|
||||
isOfficialReply: isSuperAdmin || false,
|
||||
author: { id: authorId },
|
||||
review,
|
||||
parentReply,
|
||||
});
|
||||
|
||||
await this.replyRepository.save(reply);
|
||||
|
||||
return {
|
||||
message: ServiceMessage.REVIEW_REPLY_ADDED,
|
||||
reply,
|
||||
};
|
||||
}
|
||||
|
||||
//########################################################
|
||||
|
||||
async getDanakServiceReviews(queryDto: DanakServiceReviewQueryDto) {
|
||||
const [reviews, count] = await this.danakServiceReviewRepository.getDanakServiceReviews(queryDto);
|
||||
|
||||
return {
|
||||
reviews,
|
||||
count,
|
||||
paginate: true,
|
||||
};
|
||||
}
|
||||
|
||||
/******************************************** */
|
||||
|
||||
async addReview(userId: string, serviceId: string, addReviewDto: AddReviewDto) {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
|
||||
try {
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
const user = await this.usersService.findOneByIdWithQueryRunner(userId, queryRunner);
|
||||
|
||||
const service = await queryRunner.manager.findOne(this.danakServicesRepository.target, {
|
||||
where: { id: serviceId, deletedAt: IsNull() },
|
||||
});
|
||||
|
||||
if (!service) throw new BadRequestException(ServiceMessage.SERVICE_NOT_EXIST);
|
||||
|
||||
const hasSubscription = await this.checkUserPurchasedService(userId, serviceId);
|
||||
if (!hasSubscription) throw new BadRequestException(SubscriptionMessage.NOT_PURCHASED_CANNOT_REVIEW);
|
||||
|
||||
const review = queryRunner.manager.create(this.danakServiceReviewRepository.target, {
|
||||
user: { id: userId },
|
||||
...addReviewDto,
|
||||
service,
|
||||
});
|
||||
|
||||
// TODO: remove this after a while
|
||||
review.status = ReviewStatus.APPROVED;
|
||||
|
||||
await queryRunner.manager.save(this.danakServiceReviewRepository.target, review);
|
||||
|
||||
const superAdmins = await this.adminsService.getSuperAdmins(queryRunner);
|
||||
|
||||
for (const admin of superAdmins) {
|
||||
await this.notificationQueue.addNewServiceReviewNotification(admin.id, {
|
||||
serviceName: service.name,
|
||||
userPhone: admin.phone,
|
||||
userEmail: admin.email,
|
||||
fullName: `${user.firstName} ${user.lastName}`,
|
||||
});
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return {
|
||||
message: ServiceMessage.REVIEW_ADDED,
|
||||
review,
|
||||
};
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error("Error in add review", error);
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************** */
|
||||
|
||||
async updateDanakServiceReviewStatus(updateParamDto: UpdateReviewStatusDto) {
|
||||
if (updateParamDto.status === ReviewStatus.APPROVED) {
|
||||
return await this.approveReview(updateParamDto.id);
|
||||
} else {
|
||||
return await this.rejectReview(updateParamDto.id);
|
||||
}
|
||||
}
|
||||
/******************************************** */
|
||||
|
||||
async approveReview(reviewId: string) {
|
||||
const review = await this.danakServiceReviewRepository.findOneBy({ id: reviewId });
|
||||
if (!review) throw new BadRequestException(ServiceMessage.REVIEW_NOT_EXIST);
|
||||
|
||||
review.status = ReviewStatus.APPROVED;
|
||||
|
||||
await this.danakServiceReviewRepository.save(review);
|
||||
|
||||
return {
|
||||
message: ServiceMessage.REVIEW_APPROVED,
|
||||
review,
|
||||
};
|
||||
}
|
||||
/******************************************** */
|
||||
|
||||
async rejectReview(reviewId: string) {
|
||||
const review = await this.danakServiceReviewRepository.findOneBy({ id: reviewId });
|
||||
if (!review) throw new BadRequestException(ServiceMessage.REVIEW_NOT_EXIST);
|
||||
|
||||
review.status = ReviewStatus.REJECTED;
|
||||
|
||||
await this.danakServiceReviewRepository.save(review);
|
||||
|
||||
return {
|
||||
message: ServiceMessage.REVIEW_REJECTED,
|
||||
review,
|
||||
};
|
||||
}
|
||||
|
||||
//########################################################
|
||||
private async checkUserPurchasedService(userId: string, serviceId: string, queryRunner?: QueryRunner) {
|
||||
const subscription = await (queryRunner ? queryRunner.manager : this.danakServicesRepository)
|
||||
.createQueryBuilder()
|
||||
.select("1")
|
||||
.from("user_subscription", "us")
|
||||
.innerJoin("subscription_plan", "sp", "us.planId = sp.id")
|
||||
.where("us.userId = :userId", { userId })
|
||||
.andWhere("sp.serviceId = :serviceId", { serviceId })
|
||||
.andWhere(`(us.status = '${SubscriptionStatus.ACTIVE}')`)
|
||||
.andWhere("us.endDate > NOW()")
|
||||
.andWhere("us.startDate < NOW()")
|
||||
.limit(1)
|
||||
.getRawOne();
|
||||
|
||||
return !!subscription;
|
||||
}
|
||||
}
|
||||
@@ -2,32 +2,25 @@ import { BadRequestException, Injectable, Logger } from "@nestjs/common";
|
||||
import { DataSource, FindOptionsWhere, In, IsNull, Not, QueryRunner } from "typeorm";
|
||||
|
||||
import { ParamDto } from "../../../common/DTO/param.dto";
|
||||
import { CategoryMessage, CommonMessage, ServiceMessage, SubscriptionMessage } from "../../../common/enums/message.enum";
|
||||
import { NotificationQueue } from "../../notifications/queue/notification.queue";
|
||||
import { CategoryMessage, CommonMessage, ServiceMessage } from "../../../common/enums/message.enum";
|
||||
import { SubscriptionStatus } from "../../subscriptions/enums/subscription-status.enum";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
import { AdminsService } from "../../users/providers/admins.service";
|
||||
import { UsersService } from "../../users/providers/users.service";
|
||||
import { PaginationUtils } from "../../utils/providers/pagination.utils";
|
||||
import { AddReviewDto } from "../DTO/add-review.dto";
|
||||
import { CategoryListSearchQueryDto, CategorySearchQueryDto } from "../DTO/category-search-query.dto";
|
||||
import { CreateDanakServiceCategoryDto } from "../DTO/create-category.dto";
|
||||
import { CreateServiceFeedbackDto } from "../DTO/create-service-feedback.dto";
|
||||
import { CreateServiceGuideDto } from "../DTO/create-service-guide.dto";
|
||||
import { CreateServiceDto } from "../DTO/create-service.dto";
|
||||
import { DanakServiceReviewQueryDto } from "../DTO/danak-service-review-query.dto";
|
||||
import { DanakServicesGlobalSearchDto, DanakServicesQueryDto, DanakServicesSearchQueryDto } from "../DTO/danak-services-search-query.dto";
|
||||
import { ServiceFeedbackQueryDto } from "../DTO/service-feedback-query.dto";
|
||||
import { ServiceGuideQueryDto } from "../DTO/service-guide-query.dto";
|
||||
import { UpdateDanakServiceCategoryDto } from "../DTO/update-category.dto";
|
||||
import { UpdateReviewStatusDto } from "../DTO/update-review-status.dto";
|
||||
import { UpdateServiceGuideDto } from "../DTO/update-service-guide.dto";
|
||||
import { UpdateServiceDto } from "../DTO/update-service.dto";
|
||||
import { DanakServiceAudio } from "../entities/danak-service-audio.entity";
|
||||
import { DanakServiceCategory } from "../entities/danak-service-category.entity";
|
||||
import { DanakServiceImage } from "../entities/danak-service-image.entity";
|
||||
import { DanakServiceVideo } from "../entities/danak-service-video.entity";
|
||||
import { ReviewStatus } from "../enums/review-status.enum";
|
||||
import { DanakServiceFeedbackRepository } from "../repositories/danak-service-feedback.repository";
|
||||
import { DanakServiceGuideRepository } from "../repositories/danak-service-guide.repository";
|
||||
import { DanakServicesAudioRepository } from "../repositories/danak-services-audio.repository";
|
||||
@@ -49,9 +42,6 @@ export class DanakServicesService {
|
||||
private readonly danakServicesAudioRepository: DanakServicesAudioRepository,
|
||||
private readonly danakServicesVideoRepository: DanakServicesVideoRepository,
|
||||
private readonly dataSource: DataSource,
|
||||
private readonly adminsService: AdminsService,
|
||||
private readonly usersService: UsersService,
|
||||
private readonly notificationQueue: NotificationQueue,
|
||||
) {}
|
||||
/******************************************** */
|
||||
|
||||
@@ -531,18 +521,6 @@ export class DanakServicesService {
|
||||
|
||||
/******************************************** */
|
||||
|
||||
async getDanakServiceReviews(queryDto: DanakServiceReviewQueryDto) {
|
||||
const [reviews, count] = await this.danakServiceReviewRepository.getDanakServiceReviews(queryDto);
|
||||
|
||||
return {
|
||||
reviews,
|
||||
count,
|
||||
paginate: true,
|
||||
};
|
||||
}
|
||||
|
||||
/******************************************** */
|
||||
|
||||
async getServiceByName(name: string) {
|
||||
const danakService = await this.danakServicesRepository.findOneByName(name);
|
||||
if (!danakService) throw new BadRequestException(ServiceMessage.SERVICE_NOT_EXIST);
|
||||
@@ -566,98 +544,6 @@ export class DanakServicesService {
|
||||
|
||||
/******************************************** */
|
||||
|
||||
async addReview(userId: string, serviceId: string, addReviewDto: AddReviewDto) {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
|
||||
try {
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
const user = await this.usersService.findOneByIdWithQueryRunner(userId, queryRunner);
|
||||
|
||||
const service = await queryRunner.manager.findOne(this.danakServicesRepository.target, {
|
||||
where: { id: serviceId, deletedAt: IsNull() },
|
||||
});
|
||||
|
||||
if (!service) throw new BadRequestException(ServiceMessage.SERVICE_NOT_EXIST);
|
||||
|
||||
const hasSubscription = await this.checkUserPurchasedService(userId, serviceId);
|
||||
if (!hasSubscription) throw new BadRequestException(SubscriptionMessage.NOT_PURCHASED_CANNOT_REVIEW);
|
||||
|
||||
const review = queryRunner.manager.create(this.danakServiceReviewRepository.target, {
|
||||
user: { id: userId },
|
||||
...addReviewDto,
|
||||
service,
|
||||
});
|
||||
|
||||
await queryRunner.manager.save(this.danakServiceReviewRepository.target, review);
|
||||
|
||||
const superAdmins = await this.adminsService.getSuperAdmins(queryRunner);
|
||||
|
||||
for (const admin of superAdmins) {
|
||||
await this.notificationQueue.addNewServiceReviewNotification(admin.id, {
|
||||
serviceName: service.name,
|
||||
userPhone: admin.phone,
|
||||
userEmail: admin.email,
|
||||
fullName: `${user.firstName} ${user.lastName}`,
|
||||
});
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return {
|
||||
message: ServiceMessage.REVIEW_ADDED,
|
||||
review,
|
||||
};
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
this.logger.error("Error in add review", error);
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************** */
|
||||
|
||||
async updateDanakServiceReviewStatus(updateParamDto: UpdateReviewStatusDto) {
|
||||
if (updateParamDto.status === ReviewStatus.APPROVED) {
|
||||
return await this.approveReview(updateParamDto.id);
|
||||
} else {
|
||||
return await this.rejectReview(updateParamDto.id);
|
||||
}
|
||||
}
|
||||
|
||||
async approveReview(reviewId: string) {
|
||||
const review = await this.danakServiceReviewRepository.findOneBy({ id: reviewId });
|
||||
if (!review) throw new BadRequestException(ServiceMessage.REVIEW_NOT_EXIST);
|
||||
|
||||
review.status = ReviewStatus.APPROVED;
|
||||
|
||||
await this.danakServiceReviewRepository.save(review);
|
||||
|
||||
return {
|
||||
message: ServiceMessage.REVIEW_APPROVED,
|
||||
review,
|
||||
};
|
||||
}
|
||||
/******************************************** */
|
||||
|
||||
async rejectReview(reviewId: string) {
|
||||
const review = await this.danakServiceReviewRepository.findOneBy({ id: reviewId });
|
||||
if (!review) throw new BadRequestException(ServiceMessage.REVIEW_NOT_EXIST);
|
||||
|
||||
review.status = ReviewStatus.REJECTED;
|
||||
|
||||
await this.danakServiceReviewRepository.save(review);
|
||||
|
||||
return {
|
||||
message: ServiceMessage.REVIEW_REJECTED,
|
||||
review,
|
||||
};
|
||||
}
|
||||
|
||||
/******************************************** */
|
||||
|
||||
async getServicesCount() {
|
||||
const count = await this.danakServicesRepository.count({
|
||||
where: {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
|
||||
import { DanakServiceReviewReply } from "../entities/danak-service-review-reply.entity";
|
||||
|
||||
@Injectable()
|
||||
export class DanakServiceReviewReplyRepository extends Repository<DanakServiceReviewReply> {
|
||||
constructor(@InjectRepository(DanakServiceReviewReply) repository: Repository<DanakServiceReviewReply>) {
|
||||
super(repository.target, repository.manager, repository.queryRunner);
|
||||
}
|
||||
|
||||
async findByReviewId(reviewId: string): Promise<DanakServiceReviewReply[]> {
|
||||
return this.createQueryBuilder("reply")
|
||||
.leftJoinAndSelect("reply.author", "author")
|
||||
.leftJoinAndSelect("reply.parentReply", "parentReply")
|
||||
.leftJoinAndSelect("reply.replies", "replies")
|
||||
.where("reply.review.id = :reviewId", { reviewId })
|
||||
.andWhere("reply.parentReply IS NULL")
|
||||
.orderBy("reply.createdAt", "ASC")
|
||||
.getMany();
|
||||
}
|
||||
|
||||
async findRepliesByParentId(parentId: string): Promise<DanakServiceReviewReply[]> {
|
||||
return this.createQueryBuilder("reply")
|
||||
.leftJoinAndSelect("reply.author", "author")
|
||||
.where("reply.parentReply.id = :parentId", { parentId })
|
||||
.orderBy("reply.createdAt", "ASC")
|
||||
.getMany();
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,8 @@ export class DanakServiceReviewRepository extends Repository<DanakServiceReview>
|
||||
|
||||
const reviewsQueryBuilder = this.createQueryBuilder("review")
|
||||
.leftJoinAndSelect("review.user", "user")
|
||||
.leftJoinAndSelect("review.replies", "replies")
|
||||
.leftJoinAndSelect("replies.author", "repliesAuthor")
|
||||
.leftJoinAndSelect("review.service", "service")
|
||||
.select([
|
||||
"review.id",
|
||||
@@ -42,6 +44,18 @@ export class DanakServiceReviewRepository extends Repository<DanakServiceReview>
|
||||
"user.profilePic",
|
||||
"service.id",
|
||||
"service.name",
|
||||
"replies.id",
|
||||
"replies.content",
|
||||
"replies.isOfficialReply",
|
||||
"replies.status",
|
||||
"replies.createdAt",
|
||||
"replies.updatedAt",
|
||||
"repliesAuthor.id",
|
||||
"repliesAuthor.firstName",
|
||||
"repliesAuthor.lastName",
|
||||
"repliesAuthor.profilePic",
|
||||
"repliesAuthor.email",
|
||||
"repliesAuthor.phone",
|
||||
])
|
||||
.orderBy("review.createdAt", "DESC")
|
||||
.skip(skip)
|
||||
|
||||
@@ -101,6 +101,8 @@ export class DanakServicesRepository extends Repository<DanakService> {
|
||||
.leftJoinAndSelect("subscriptionPlans.directDiscount", "directDiscount", "directDiscount.id IS NOT NULL")
|
||||
.leftJoin("service.reviews", "reviews", "reviews.status = :reviewStatus", { reviewStatus: ReviewStatus.APPROVED })
|
||||
.leftJoin("reviews.user", "user")
|
||||
.leftJoinAndSelect("reviews.replies", "replies")
|
||||
.leftJoinAndSelect("replies.author", "repliesAuthor")
|
||||
.addSelect([
|
||||
"reviews.id",
|
||||
"reviews.comment",
|
||||
@@ -112,6 +114,17 @@ export class DanakServicesRepository extends Repository<DanakService> {
|
||||
"user.firstName",
|
||||
"user.lastName",
|
||||
"user.profilePic",
|
||||
"replies.id",
|
||||
"replies.content",
|
||||
"replies.isOfficialReply",
|
||||
"replies.status",
|
||||
"replies.createdAt",
|
||||
"repliesAuthor.id",
|
||||
"repliesAuthor.firstName",
|
||||
"repliesAuthor.lastName",
|
||||
"repliesAuthor.profilePic",
|
||||
"repliesAuthor.email",
|
||||
"repliesAuthor.phone",
|
||||
])
|
||||
// .loadRelationCountAndMap("service.reviewCount", "service.reviews")
|
||||
// .addSelect(
|
||||
@@ -124,7 +137,9 @@ export class DanakServicesRepository extends Repository<DanakService> {
|
||||
// "averageRating",
|
||||
// )
|
||||
.andWhere("service.id = :serviceId", { serviceId })
|
||||
.groupBy("service.id,category.id, images.id,audios.id,videos.id, subscriptionPlans.id, reviews.id, user.id, directDiscount.id");
|
||||
.groupBy(
|
||||
"service.id,category.id, images.id,audios.id,videos.id, subscriptionPlans.id, reviews.id, user.id, directDiscount.id, replies.id, repliesAuthor.id",
|
||||
);
|
||||
|
||||
if (!isAdmin) {
|
||||
serviceQueryBuilder
|
||||
@@ -149,6 +164,8 @@ export class DanakServicesRepository extends Repository<DanakService> {
|
||||
.leftJoinAndSelect("subscriptionPlans.directDiscount", "directDiscount", "directDiscount.id IS NOT NULL")
|
||||
.leftJoin("service.reviews", "reviews", "reviews.status = :reviewStatus", { reviewStatus: ReviewStatus.APPROVED })
|
||||
.leftJoin("reviews.user", "user")
|
||||
.leftJoinAndSelect("reviews.replies", "replies")
|
||||
.leftJoinAndSelect("replies.author", "repliesAuthor")
|
||||
.addSelect([
|
||||
"reviews.id",
|
||||
"reviews.comment",
|
||||
@@ -160,6 +177,17 @@ export class DanakServicesRepository extends Repository<DanakService> {
|
||||
"user.firstName",
|
||||
"user.lastName",
|
||||
"user.profilePic",
|
||||
"replies.id",
|
||||
"replies.content",
|
||||
"replies.isOfficialReply",
|
||||
"replies.status",
|
||||
"replies.createdAt",
|
||||
"repliesAuthor.id",
|
||||
"repliesAuthor.firstName",
|
||||
"repliesAuthor.lastName",
|
||||
"repliesAuthor.profilePic",
|
||||
"repliesAuthor.email",
|
||||
"repliesAuthor.phone",
|
||||
])
|
||||
// .loadRelationCountAndMap("service.reviewCount", "service.reviews")
|
||||
// .addSelect(
|
||||
@@ -172,7 +200,9 @@ export class DanakServicesRepository extends Repository<DanakService> {
|
||||
// "averageRating",
|
||||
// )
|
||||
.andWhere("service.slug = :slug", { slug })
|
||||
.groupBy("service.id,category.id, images.id,audios.id,videos.id, subscriptionPlans.id, reviews.id, user.id, directDiscount.id");
|
||||
.groupBy(
|
||||
"service.id,category.id, images.id,audios.id,videos.id, subscriptionPlans.id, reviews.id, user.id, directDiscount.id, replies.id, repliesAuthor.id",
|
||||
);
|
||||
|
||||
if (!isAdmin) {
|
||||
serviceQueryBuilder
|
||||
@@ -204,7 +234,7 @@ export class DanakServicesRepository extends Repository<DanakService> {
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
//+********************
|
||||
async getDanakSuggestServices() {
|
||||
return this.find({
|
||||
where: { isDanakSuggest: true, isActive: true, deletedAt: IsNull(), category: { isActive: true, deletedAt: IsNull() } },
|
||||
@@ -232,7 +262,7 @@ export class DanakServicesRepository extends Repository<DanakService> {
|
||||
withDeleted: false,
|
||||
});
|
||||
}
|
||||
|
||||
//+********************
|
||||
async searchServices(queryDto: DanakServicesGlobalSearchDto) {
|
||||
const servicesQuery = this.createQueryBuilder("service")
|
||||
.leftJoin("service.category", "category")
|
||||
|
||||
Reference in New Issue
Block a user