chore: add comment reply for review and blog comment

This commit is contained in:
mahyargdz
2025-08-31 09:55:33 +03:30
parent 9c409bef27
commit 8e9b939a0b
20 changed files with 624 additions and 222 deletions
+14
View File
@@ -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;
}
+16 -5
View File
@@ -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 --------------
+14 -3
View File
@@ -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 -84
View File
@@ -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")