diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 8125910..0b994aa 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -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 { diff --git a/src/modules/blogs/DTO/create-comment-reply.dto.ts b/src/modules/blogs/DTO/create-comment-reply.dto.ts new file mode 100644 index 0000000..2014c2a --- /dev/null +++ b/src/modules/blogs/DTO/create-comment-reply.dto.ts @@ -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; +} diff --git a/src/modules/blogs/blogs.controller.ts b/src/modules/blogs/blogs.controller.ts index 4015073..8f1e9db 100755 --- a/src/modules/blogs/blogs.controller.ts +++ b/src/modules/blogs/blogs.controller.ts @@ -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 -------------- diff --git a/src/modules/blogs/blogs.module.ts b/src/modules/blogs/blogs.module.ts index 2c98b5a..913875a 100755 --- a/src/modules/blogs/blogs.module.ts +++ b/src/modules/blogs/blogs.module.ts @@ -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 {} diff --git a/src/modules/blogs/entities/blog-comment-reply.entity.ts b/src/modules/blogs/entities/blog-comment-reply.entity.ts new file mode 100644 index 0000000..665ec38 --- /dev/null +++ b/src/modules/blogs/entities/blog-comment-reply.entity.ts @@ -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[]; +} diff --git a/src/modules/blogs/entities/blog-comment.entity.ts b/src/modules/blogs/entities/blog-comment.entity.ts index 633e8df..f5a5f39 100755 --- a/src/modules/blogs/entities/blog-comment.entity.ts +++ b/src/modules/blogs/entities/blog-comment.entity.ts @@ -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[]; } diff --git a/src/modules/blogs/providers/blog-comment.service.ts b/src/modules/blogs/providers/blog-comment.service.ts new file mode 100644 index 0000000..5d37b44 --- /dev/null +++ b/src/modules/blogs/providers/blog-comment.service.ts @@ -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 }; + } +} diff --git a/src/modules/blogs/providers/blogs.service.ts b/src/modules/blogs/providers/blogs.service.ts index 8e34127..fc19966 100755 --- a/src/modules/blogs/providers/blogs.service.ts +++ b/src/modules/blogs/providers/blogs.service.ts @@ -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 { diff --git a/src/modules/blogs/repositories/blog-comment-reply.repository.ts b/src/modules/blogs/repositories/blog-comment-reply.repository.ts new file mode 100644 index 0000000..95fb0ed --- /dev/null +++ b/src/modules/blogs/repositories/blog-comment-reply.repository.ts @@ -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 { + constructor(@InjectRepository(BlogCommentReply) repository: Repository) { + super(repository.target, repository.manager, repository.queryRunner); + } + //+******************** + async findByCommentId(commentId: string): Promise { + 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 { + return this.createQueryBuilder("reply") + .leftJoinAndSelect("reply.author", "author") + .where("reply.parentReply.id = :parentId", { parentId }) + .orderBy("reply.createdAt", "ASC") + .getMany(); + } +} diff --git a/src/modules/blogs/repositories/blog-comments.repository.ts b/src/modules/blogs/repositories/blog-comments.repository.ts index ac7c055..9c51fa6 100755 --- a/src/modules/blogs/repositories/blog-comments.repository.ts +++ b/src/modules/blogs/repositories/blog-comments.repository.ts @@ -25,6 +25,7 @@ export class BlogCommentsRepository extends Repository { order: { createdAt: "DESC" }, relations: { user: true, + replies: true, }, select: { id: true, @@ -33,6 +34,7 @@ export class BlogCommentsRepository extends Repository { 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 { .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 { 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 { 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 } }, }, }); } diff --git a/src/modules/danak-services/DTO/add-review-reply.dto.ts b/src/modules/danak-services/DTO/add-review-reply.dto.ts new file mode 100644 index 0000000..5c073f0 --- /dev/null +++ b/src/modules/danak-services/DTO/add-review-reply.dto.ts @@ -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; +} diff --git a/src/modules/danak-services/danak-services.controller.ts b/src/modules/danak-services/danak-services.controller.ts index 7ac0f95..b92affe 100755 --- a/src/modules/danak-services/danak-services.controller.ts +++ b/src/modules/danak-services/danak-services.controller.ts @@ -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 ------------------------ diff --git a/src/modules/danak-services/danak-services.module.ts b/src/modules/danak-services/danak-services.module.ts index 8394e21..27dfbde 100755 --- a/src/modules/danak-services/danak-services.module.ts +++ b/src/modules/danak-services/danak-services.module.ts @@ -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 {} diff --git a/src/modules/danak-services/entities/danak-service-review-reply.entity.ts b/src/modules/danak-services/entities/danak-service-review-reply.entity.ts new file mode 100644 index 0000000..5eeaba4 --- /dev/null +++ b/src/modules/danak-services/entities/danak-service-review-reply.entity.ts @@ -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; +} diff --git a/src/modules/danak-services/entities/danak-service-review.entity.ts b/src/modules/danak-services/entities/danak-service-review.entity.ts index a9bd4e6..db53e38 100755 --- a/src/modules/danak-services/entities/danak-service-review.entity.ts +++ b/src/modules/danak-services/entities/danak-service-review.entity.ts @@ -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[]; } diff --git a/src/modules/danak-services/providers/danak-service-review.service.ts b/src/modules/danak-services/providers/danak-service-review.service.ts new file mode 100644 index 0000000..cd3eb57 --- /dev/null +++ b/src/modules/danak-services/providers/danak-service-review.service.ts @@ -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; + } +} diff --git a/src/modules/danak-services/providers/danak-services.service.ts b/src/modules/danak-services/providers/danak-services.service.ts index b7b9ced..efa1c31 100755 --- a/src/modules/danak-services/providers/danak-services.service.ts +++ b/src/modules/danak-services/providers/danak-services.service.ts @@ -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: { diff --git a/src/modules/danak-services/repositories/danak-service-review-reply.repository.ts b/src/modules/danak-services/repositories/danak-service-review-reply.repository.ts new file mode 100644 index 0000000..81d73f2 --- /dev/null +++ b/src/modules/danak-services/repositories/danak-service-review-reply.repository.ts @@ -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 { + constructor(@InjectRepository(DanakServiceReviewReply) repository: Repository) { + super(repository.target, repository.manager, repository.queryRunner); + } + + async findByReviewId(reviewId: string): Promise { + 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 { + return this.createQueryBuilder("reply") + .leftJoinAndSelect("reply.author", "author") + .where("reply.parentReply.id = :parentId", { parentId }) + .orderBy("reply.createdAt", "ASC") + .getMany(); + } +} diff --git a/src/modules/danak-services/repositories/danak-services-review.repository.ts b/src/modules/danak-services/repositories/danak-services-review.repository.ts index 690ff4d..0f8b81e 100755 --- a/src/modules/danak-services/repositories/danak-services-review.repository.ts +++ b/src/modules/danak-services/repositories/danak-services-review.repository.ts @@ -28,6 +28,8 @@ export class DanakServiceReviewRepository extends Repository 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 "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) diff --git a/src/modules/danak-services/repositories/danak-services.repository.ts b/src/modules/danak-services/repositories/danak-services.repository.ts index 538cf42..6f49d28 100755 --- a/src/modules/danak-services/repositories/danak-services.repository.ts +++ b/src/modules/danak-services/repositories/danak-services.repository.ts @@ -101,6 +101,8 @@ export class DanakServicesRepository extends Repository { .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 { "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 { // "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 { .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 { "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 { // "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 { 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 { withDeleted: false, }); } - + //+******************** async searchServices(queryDto: DanakServicesGlobalSearchDto) { const servicesQuery = this.createQueryBuilder("service") .leftJoin("service.category", "category")