From b7cfaf95b290210478b881d97896411a12246aaa Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Mon, 17 Feb 2025 15:08:22 +0330 Subject: [PATCH] chore: add service review add and get in service page and also approve and reject for admin --- .env.example | 8 +- src/common/enums/message.enum.ts | 15 ++- .../danak-services/DTO/add-review.dto.ts | 29 ++++++ .../danak-services/DTO/create-service.dto.ts | 8 +- .../danak-services.controller.ts | 8 ++ .../danak-services/danak-services.module.ts | 12 ++- .../entities/danak-service-review.entity.ts | 27 +++++ .../entities/danak-service.entity.ts | 6 +- .../enums/review-status.enum.ts | 5 + .../providers/danak-services.service.ts | 99 ++++++++++++------- .../danak-services-review.repository.ts | 12 +++ src/modules/users/entities/user.entity.ts | 4 + 12 files changed, 186 insertions(+), 47 deletions(-) create mode 100644 src/modules/danak-services/DTO/add-review.dto.ts create mode 100644 src/modules/danak-services/entities/danak-service-review.entity.ts create mode 100644 src/modules/danak-services/enums/review-status.enum.ts create mode 100644 src/modules/danak-services/repositories/danak-services-review.repository.ts diff --git a/.env.example b/.env.example index 03e43c2..f5743d7 100644 --- a/.env.example +++ b/.env.example @@ -13,9 +13,9 @@ PGADMIN_EMAIL=root@test.ir PGADMIN_PASSWORD=password -SMS_API_URL= https://api.sms.ir/v1 -SMS_API_KEY="GoqtKIterHPHTRlmBtv3JTVU06a4a6YvcyCWcv0ISojcpmNW" -SMS_PATTERN_OTP=123456 +SMS_API_URL=https://api.sms.ir/v1 +SMS_API_KEY="dasdas" +SMS_PATTERN_OTP=82385 SMTP_HOST= SMTP_PORT= @@ -51,4 +51,4 @@ IPG_TYPE=sandbox ZARINPAL_MERCHANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx SITE_URL=http://localhost:4000/payment ZARINPAL_API_URL=zarinpal.com/pg -CALLBACK_URL=http://localhost:4000/payments/verify \ No newline at end of file +CALLBACK_URL=http://localhost:4000/payments/verify diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 4dc9d4b..ee5f54a 100644 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -100,7 +100,7 @@ export const enum CategoryMessage { export const enum ServiceMessage { NAME_REQUIRED = "نام سرویس مورد نیاز است", NAME_STRING = "نام سرویس باید یک رشته باشد", - NAME_LENGTH = "نام سرویس باید بین ۳ تا ۵۰ کاراکتر باشد", + NAME_LENGTH = "نام سرویس باید بین ۳ تا ۱۵۰ کاراکتر باشد", DESCRIPTION_REQUIRED = "توضیحات سرویس مورد نیاز است", DESCRIPTION_STRING = "توضیحات سرویس باید یک رشته باشد", AUTHOR_REQUIRED = "نویسنده سرویس مورد نیاز است", @@ -128,6 +128,19 @@ export const enum ServiceMessage { SERVICE_NOT_FOUND_BY_ID = "سرویسی با این شناسه یافت نشد یا سرویس غیرفعال می‌باشد", SERVICE_ID_SHOULD_BE_A_UUID = "شناسه سرویس باید یک UUID باشد", SERVICE_ID_REQUIRED = "شناسه سرویس مورد نیاز است", + COMMENT_REQUIRED = "نظر سرویس مورد نیاز است", + COMMENT_STRING = "نظر سرویس باید یک رشته باشد", + COMMENT_LENGTH = "نظر سرویس باید بین ۱ تا ۴۰۰ کاراکتر باشد", + RATING_REQUIRED = "امتیاز سرویس مورد نیاز است", + RATING_INT = "امتیاز سرویس باید یک عدد صحیح باشد", + RATING_RANGE = "امتیاز سرویس باید بین ۱ تا ۵ باشد", + TITLE_REQUIRED = "عنوان سرویس مورد نیاز است", + TITLE_STRING = "عنوان سرویس باید یک رشته باشد", + TITLE_LENGTH = "عنوان سرویس باید بین ۱ تا ۱۵۰ کاراکتر باشد", + REVIEW_ADDED = "نظر شما با موفقیت ثبت شد", + REVIEW_NOT_EXIST = "نظری برای این سرویس یافت نشد", + REVIEW_APPROVED = "نظر با موفقیت تایید شد", + REVIEW_REJECTED = "نظر با موفقیت رد شد", } export const enum AnnouncementMessage { diff --git a/src/modules/danak-services/DTO/add-review.dto.ts b/src/modules/danak-services/DTO/add-review.dto.ts new file mode 100644 index 0000000..8d88b77 --- /dev/null +++ b/src/modules/danak-services/DTO/add-review.dto.ts @@ -0,0 +1,29 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsIn, IsInt, IsNotEmpty, IsString, Length } from "class-validator"; + +import { ServiceMessage } from "../../../common/enums/message.enum"; + +export class AddReviewDto { + @IsNotEmpty({ message: ServiceMessage.COMMENT_REQUIRED }) + @IsString({ message: ServiceMessage.COMMENT_STRING }) + @Length(1, 400, { message: ServiceMessage.COMMENT_LENGTH }) + @ApiProperty({ description: "comment of review", example: "This is a comment" }) + comment: string; + + @IsNotEmpty({ message: ServiceMessage.RATING_REQUIRED }) + @IsInt({ message: ServiceMessage.RATING_INT }) + @IsIn([1, 2, 3, 4, 5], { message: ServiceMessage.RATING_RANGE }) + @ApiProperty({ description: "rating of review", example: 5 }) + rating: number; + + @IsNotEmpty({ message: ServiceMessage.TITLE_REQUIRED }) + @IsString({ message: ServiceMessage.TITLE_STRING }) + @Length(1, 100, { message: ServiceMessage.TITLE_LENGTH }) + @ApiProperty({ description: "title of review", example: "This is a title" }) + title: string; + + // @IsNotEmpty({ message: ServiceMessage.SERVICE_ID_REQUIRED }) + // @IsUUID("4", { message: ServiceMessage.SERVICE_ID_SHOULD_BE_A_UUID }) + // @ApiProperty({ description: "Service ID", example: "d290f1ee-6c54-4b01-90e6-d701748f0851" }) + // serviceId: string; +} diff --git a/src/modules/danak-services/DTO/create-service.dto.ts b/src/modules/danak-services/DTO/create-service.dto.ts index b73219d..5a024a0 100644 --- a/src/modules/danak-services/DTO/create-service.dto.ts +++ b/src/modules/danak-services/DTO/create-service.dto.ts @@ -12,10 +12,16 @@ export class CreateServiceDto { @IsNotEmpty({ message: ServiceMessage.NAME_REQUIRED }) @IsString({ message: ServiceMessage.NAME_STRING }) - @Length(3, 50, { message: ServiceMessage.NAME_LENGTH }) + @Length(3, 150, { message: ServiceMessage.NAME_LENGTH }) @ApiProperty({ description: "The name of the service", example: "Service Name" }) name: string; + @IsNotEmpty({ message: ServiceMessage.TITLE_REQUIRED }) + @IsString({ message: ServiceMessage.TITLE_STRING }) + @Length(3, 150, { message: ServiceMessage.TITLE_LENGTH }) + @ApiProperty({ description: "The TITLE of the service", example: "Service TITLE" }) + title: string; + @IsNotEmpty({ message: ServiceMessage.DESCRIPTION_REQUIRED }) @IsString({ message: ServiceMessage.DESCRIPTION_STRING }) @Length(10, 500, { message: ServiceMessage.DESCRIPTION_LENGTH }) diff --git a/src/modules/danak-services/danak-services.controller.ts b/src/modules/danak-services/danak-services.controller.ts index 5c7868d..9be171c 100644 --- 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, Get, HttpCode, HttpStatus, Param, Post, Query } from "@nestjs/common"; import { ApiOperation, ApiTags } from "@nestjs/swagger"; +import { AddReviewDto } from "./DTO/add-review.dto"; import { CategoryListSearchQueryDto, CategorySearchQueryDto } from "./DTO/category-search-query.dto"; import { CreateCategoryDto } from "./DTO/create-category.dto"; import { CreateServiceDto } from "./DTO/create-service.dto"; @@ -106,6 +107,13 @@ export class DanakServicesController { return this.danakServicesService.getDanakServiceByIdWithSubs(paramDto.id, isAdmin); } + @ApiOperation({ summary: "add review to danak service ==> user route" }) + @AuthGuards() + @Post(":id/reviews") + addReview(@Param() paramDto: ParamDto, @Body() addReviewDto: AddReviewDto, @UserDec("id") userId: string) { + return this.danakServicesService.addReview(userId, paramDto.id, addReviewDto); + } + // @AuthGuards() // // @ApiOperation({ summary: "get all user purchased danak services" }) diff --git a/src/modules/danak-services/danak-services.module.ts b/src/modules/danak-services/danak-services.module.ts index 224c8d5..483322e 100644 --- a/src/modules/danak-services/danak-services.module.ts +++ b/src/modules/danak-services/danak-services.module.ts @@ -4,15 +4,23 @@ import { TypeOrmModule } from "@nestjs/typeorm"; import { DanakServicesController } from "./danak-services.controller"; import { DanakServiceCategory } from "./entities/danak-service-category.entity"; import { DanakServiceImage } from "./entities/danak-service-image.entity"; +import { DanakServiceReview } from "./entities/danak-service-review.entity"; import { DanakService } from "./entities/danak-service.entity"; import { DanakServicesService } from "./providers/danak-services.service"; 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"; @Module({ - imports: [TypeOrmModule.forFeature([DanakService, DanakServiceImage, DanakServiceCategory])], - providers: [DanakServicesService, DanakServicesRepository, DanakServicesCategoryRepository, DanakServicesImageRepository], + imports: [TypeOrmModule.forFeature([DanakService, DanakServiceImage, DanakServiceCategory, DanakServiceReview])], + providers: [ + DanakServicesService, + DanakServicesRepository, + DanakServicesCategoryRepository, + DanakServicesImageRepository, + DanakServiceReviewRepository, + ], controllers: [DanakServicesController], exports: [DanakServicesService], }) diff --git a/src/modules/danak-services/entities/danak-service-review.entity.ts b/src/modules/danak-services/entities/danak-service-review.entity.ts new file mode 100644 index 0000000..eb5f402 --- /dev/null +++ b/src/modules/danak-services/entities/danak-service-review.entity.ts @@ -0,0 +1,27 @@ +import { Column, Entity, ManyToOne } from "typeorm"; + +import { DanakService } from "./danak-service.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 DanakServiceReview extends BaseEntity { + @Column({ type: "text", nullable: false }) + comment: string; + + @Column({ type: "int", nullable: false }) + rating: number; + + @Column({ type: "varchar", length: 150, nullable: false }) + title: string; + + @Column({ type: "enum", enum: ReviewStatus, default: ReviewStatus.PENDING }) + status: ReviewStatus; + + @ManyToOne(() => User, (user) => user.reviews, { onDelete: "SET NULL" }) + user: User; + + @ManyToOne(() => DanakService, (service) => service.reviews, { onDelete: "CASCADE" }) + service: DanakService; +} diff --git a/src/modules/danak-services/entities/danak-service.entity.ts b/src/modules/danak-services/entities/danak-service.entity.ts index 7b86eb3..e9f81d8 100644 --- a/src/modules/danak-services/entities/danak-service.entity.ts +++ b/src/modules/danak-services/entities/danak-service.entity.ts @@ -2,6 +2,7 @@ import { Column, Entity, ManyToOne, OneToMany } from "typeorm"; import { DanakServiceCategory } from "./danak-service-category.entity"; import { DanakServiceImage } from "./danak-service-image.entity"; +import { DanakServiceReview } from "./danak-service-review.entity"; import { BaseEntity } from "../../../common/entities/base.entity"; import { Ads } from "../../ads/entities/ads.entity"; import { Announcement } from "../../announcements/entities/announcement.entity"; @@ -11,7 +12,7 @@ import { ServicesLanguage } from "../enums/services-language.enum"; @Entity() export class DanakService extends BaseEntity { - @Column({ type: "varchar", length: 100, nullable: false, unique: true }) + @Column({ type: "varchar", length: 150, nullable: false, unique: true }) name: string; @Column({ type: "varchar", length: 150, nullable: true }) @@ -72,6 +73,9 @@ export class DanakService extends BaseEntity { @OneToMany(() => Ads, (ads) => ads.service) ads: Ads[]; + @OneToMany(() => DanakServiceReview, (danakServiceReview) => danakServiceReview.service) + reviews: DanakServiceReview[]; + // @ManyToMany(() => Discount, (discount) => discount.services, { nullable: true }) // discounts?: Discount[]; } diff --git a/src/modules/danak-services/enums/review-status.enum.ts b/src/modules/danak-services/enums/review-status.enum.ts new file mode 100644 index 0000000..f3284db --- /dev/null +++ b/src/modules/danak-services/enums/review-status.enum.ts @@ -0,0 +1,5 @@ +export enum ReviewStatus { + PENDING = "PENDING", + APPROVED = "APPROVED", + REJECTED = "REJECTED", +} diff --git a/src/modules/danak-services/providers/danak-services.service.ts b/src/modules/danak-services/providers/danak-services.service.ts index 8a1f809..e805378 100644 --- a/src/modules/danak-services/providers/danak-services.service.ts +++ b/src/modules/danak-services/providers/danak-services.service.ts @@ -4,12 +4,15 @@ import { FindOptionsWhere, In, IsNull } from "typeorm"; import { ParamDto } from "../../../common/DTO/param.dto"; import { CategoryMessage, CommonMessage, ServiceMessage } from "../../../common/enums/message.enum"; import { PaginationUtils } from "../../utils/providers/pagination.utils"; +import { AddReviewDto } from "../DTO/add-review.dto"; import { CategoryListSearchQueryDto, CategorySearchQueryDto } from "../DTO/category-search-query.dto"; import { CreateCategoryDto } from "../DTO/create-category.dto"; import { CreateServiceDto } from "../DTO/create-service.dto"; import { DanakServicesSearchQueryDto } from "../DTO/danak-services-search-query.dto"; import { DanakServiceCategory } from "../entities/danak-service-category.entity"; +import { ReviewStatus } from "../enums/review-status.enum"; import { DanakServicesCategoryRepository } from "../repositories/danak-services-category.repository"; +import { DanakServiceReviewRepository } from "../repositories/danak-services-review.repository"; import { DanakServicesRepository } from "../repositories/danak-services.repository"; @Injectable() @@ -18,6 +21,7 @@ export class DanakServicesService { constructor( private readonly danakServicesRepository: DanakServicesRepository, private readonly danakServicesCategoryRepository: DanakServicesCategoryRepository, + private readonly danakServiceReviewRepository: DanakServiceReviewRepository, ) {} /******************************************** */ @@ -178,8 +182,28 @@ export class DanakServicesService { relations: { images: true, subscriptionPlans: true }, }); if (!danakService) throw new BadRequestException(ServiceMessage.SERVICE_NOT_FOUND_BY_ID); + + const reviews = await this.danakServiceReviewRepository.find({ + where: { service: { id: serviceId }, status: ReviewStatus.APPROVED }, + relations: { user: true }, + select: { + id: true, + comment: true, + rating: true, + title: true, + createdAt: true, + status: true, + user: { + id: true, + firstName: true, + lastName: true, + profilePic: true, + }, + }, + }); return { danakService, + reviews, }; } @@ -208,46 +232,45 @@ export class DanakServicesService { /******************************************** */ - // async getDiscountedPrice(serviceId: string) { - // const service = await this.danakServicesRepository.findOne({ - // where: { - // id: serviceId, - // }, - // relations: ["discount"], - // }); - // if (!service) throw new BadRequestException(ServiceMessage.SERVICE_NOT_FOUND_BY_ID); + async addReview(userId: string, serviceId: string, addReviewDto: AddReviewDto) { + const service = await this.findServiceById(serviceId); - // const now = new Date(); + const review = this.danakServiceReviewRepository.create({ user: { id: userId }, ...addReviewDto, service }); + await this.danakServiceReviewRepository.save(review); + return { + message: ServiceMessage.REVIEW_ADDED, + review, + }; + } - // const serviceDiscount = service.discounts.filter( - // (d) => d.type === DiscountType.DIRECT && d.isActive && d.startDate <= now && d.endDate >= now, - // ); + /******************************************** */ - // const globalDiscounts = await this.discountRepository - // .createQueryBuilder("discount") - // .leftJoin("discount.services", "service") - // .where("discount.type = :discountType", { discountType: DiscountType.DIRECT }) - // .andWhere("discount.isActive = true") - // .andWhere("discount.startDate <= :now", { now }) - // .andWhere("discount.endDate >= :now", { now }) - // .andWhere("service.id IS NULL") - // .getMany(); + async approveReview(reviewId: string) { + const review = await this.danakServiceReviewRepository.findOneBy({ id: reviewId }); + if (!review) throw new BadRequestException(ServiceMessage.REVIEW_NOT_EXIST); - // const applicableDiscounts = [...serviceDiscount, ...globalDiscounts]; - // if (applicableDiscounts.length === 0) return service.; - // let maxDiscountValue = 0; - // for (const discount of applicableDiscounts) { - // let discountValue = 0; - // if (discount.calculationType === DiscountCalculationType.FIXED) { - // discountValue = discount.amount; - // } else if (discount.calculationType === DiscountCalculationType.PERCENTAGE) { - // discountValue = service.price * (discount.amount / 100); - // } - // if (discountValue > maxDiscountValue) { - // maxDiscountValue = discountValue; - // } - // } - // const discountedPrice = service.price - maxDiscountValue; - // return discountedPrice > 0 ? discountedPrice : 0; - // } + 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, + }; + } } diff --git a/src/modules/danak-services/repositories/danak-services-review.repository.ts b/src/modules/danak-services/repositories/danak-services-review.repository.ts new file mode 100644 index 0000000..20e71f5 --- /dev/null +++ b/src/modules/danak-services/repositories/danak-services-review.repository.ts @@ -0,0 +1,12 @@ +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; + +import { DanakServiceReview } from "../entities/danak-service-review.entity"; + +@Injectable() +export class DanakServiceReviewRepository extends Repository { + constructor(@InjectRepository(DanakServiceReview) danakServiceReviewRepository: Repository) { + super(danakServiceReviewRepository.target, danakServiceReviewRepository.manager, danakServiceReviewRepository.queryRunner); + } +} diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index f58e9f4..588e46a 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -8,6 +8,7 @@ import { UserGroup } from "./user-group.entity"; import { BaseEntity } from "../../../common/entities/base.entity"; import { UserAnnouncement } from "../../announcements/entities/user-announcement.entity"; import { Criticism } from "../../criticisms/entities/criticism.entity"; +import { DanakServiceReview } from "../../danak-services/entities/danak-service-review.entity"; import { Discount } from "../../discounts/entities/discount.entity"; import { Invoice } from "../../invoices/entities/invoice.entity"; import { LearningProgress } from "../../learnings/entities/learning-progress.entity"; @@ -61,6 +62,9 @@ export class User extends BaseEntity { //--------------------------------------- + @OneToMany(() => DanakServiceReview, (danakServiceReview) => danakServiceReview.user) + reviews: DanakServiceReview[]; + @OneToMany(() => Ticket, (ticket) => ticket.user) tickets: Ticket[];