chore: add service review add and get in service page and also approve and reject for admin

This commit is contained in:
mahyargdz
2025-02-17 15:08:22 +03:30
parent 763b6b49d9
commit b7cfaf95b2
12 changed files with 186 additions and 47 deletions
+4 -4
View File
@@ -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
CALLBACK_URL=http://localhost:4000/payments/verify
+14 -1
View File
@@ -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 {
@@ -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;
}
@@ -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 })
@@ -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" })
@@ -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],
})
@@ -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;
}
@@ -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[];
}
@@ -0,0 +1,5 @@
export enum ReviewStatus {
PENDING = "PENDING",
APPROVED = "APPROVED",
REJECTED = "REJECTED",
}
@@ -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,
};
}
}
@@ -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<DanakServiceReview> {
constructor(@InjectRepository(DanakServiceReview) danakServiceReviewRepository: Repository<DanakServiceReview>) {
super(danakServiceReviewRepository.target, danakServiceReviewRepository.manager, danakServiceReviewRepository.queryRunner);
}
}
@@ -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[];