chore: add service review add and get in service page and also approve and reject for admin
This commit is contained in:
+4
-4
@@ -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
|
||||
|
||||
@@ -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[];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user