From af9f365bc798a741cd3558cf1c778aa3a00aedd3 Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Sun, 2 Mar 2025 11:49:26 +0330 Subject: [PATCH] update: add review count and also verification from admin panel --- src/common/enums/message.enum.ts | 1 + .../DTO/danak-service-review-query.dto.ts | 3 + .../danak-services.controller.ts | 14 +- .../entities/danak-service.entity.ts | 12 +- .../providers/danak-services.service.ts | 163 ++++++++++++------ .../discounts/entities/discount.entity.ts | 2 +- .../invoices/providers/invoices.service.ts | 23 ++- .../interfaces/ISendNotificationData.ts | 1 + .../providers/notifications.service.ts | 9 +- .../entities/subscription.entity.ts | 8 +- .../providers/subscriptions.service.ts | 33 +--- src/modules/utils/interfaces/ISms.ts | 1 + src/modules/utils/providers/sms.service.ts | 3 +- 13 files changed, 175 insertions(+), 98 deletions(-) create mode 100644 src/modules/danak-services/DTO/danak-service-review-query.dto.ts diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 3de4b54..492ef3b 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -386,6 +386,7 @@ export const enum SubscriptionMessage { BUSINESS_DESCRIPTION_LENGTH = "توضیحات کسب و کار باید بین ۱۰ تا ۵۰۰ کاراکتر باشد", BUSINESS_DESCRIPTION_STRING = "توضیحات کسب و کار باید یک رشته باشد", IS_ACTIVE_SHOULD_BE_1_0 = "وضعیت اشتراک باید یکی از مقادیر ۰ و ۱ باشد", + NOT_PURCHASED_CANNOT_REVIEW = "شما این اشتراک را خریداری نکرده‌اید و نمی‌توانید نظر بدهید", } export const enum InvoiceMessage { diff --git a/src/modules/danak-services/DTO/danak-service-review-query.dto.ts b/src/modules/danak-services/DTO/danak-service-review-query.dto.ts new file mode 100644 index 0000000..647cd8f --- /dev/null +++ b/src/modules/danak-services/DTO/danak-service-review-query.dto.ts @@ -0,0 +1,3 @@ +import { PaginationDto } from "../../../common/DTO/pagination.dto"; + +export class DanakServiceReviewQueryDto extends PaginationDto {} diff --git a/src/modules/danak-services/danak-services.controller.ts b/src/modules/danak-services/danak-services.controller.ts index 9be171c..3eebc20 100755 --- a/src/modules/danak-services/danak-services.controller.ts +++ b/src/modules/danak-services/danak-services.controller.ts @@ -5,6 +5,7 @@ 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 { DanakServiceReviewQueryDto } from "./DTO/danak-service-review-query.dto"; import { DanakServicesSearchQueryDto } from "./DTO/danak-services-search-query.dto"; import { DanakServicesService } from "./providers/danak-services.service"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; @@ -103,8 +104,8 @@ export class DanakServicesController { @ApiOperation({ summary: "get danak service by id" }) @AuthGuards() @Get(":id") - getDanakServiceById(@Param() paramDto: ParamDto, @UserDec("isAdmin") isAdmin: boolean) { - return this.danakServicesService.getDanakServiceByIdWithSubs(paramDto.id, isAdmin); + getDanakServiceById(@Param() paramDto: ParamDto, @UserDec("isAdmin") isAdmin: boolean, @UserDec("id") userId: string) { + return this.danakServicesService.getDanakServiceByIdWithSubs(paramDto.id, isAdmin, userId); } @ApiOperation({ summary: "add review to danak service ==> user route" }) @@ -114,6 +115,15 @@ export class DanakServicesController { return this.danakServicesService.addReview(userId, paramDto.id, addReviewDto); } + @ApiOperation({ summary: "get danak service reviews ==> admin route" }) + @AuthGuards() + @Pagination() + @PermissionsDec(PermissionEnum.SERVICES) + @Get("reviews") + getDanakServiceReviews(@Query() queryDto: DanakServiceReviewQueryDto) { + return this.danakServicesService.getDanakServiceReviews(queryDto); + } + // @AuthGuards() // // @ApiOperation({ summary: "get all user purchased danak services" }) diff --git a/src/modules/danak-services/entities/danak-service.entity.ts b/src/modules/danak-services/entities/danak-service.entity.ts index a17c3d1..91a9f8c 100755 --- a/src/modules/danak-services/entities/danak-service.entity.ts +++ b/src/modules/danak-services/entities/danak-service.entity.ts @@ -51,6 +51,12 @@ export class DanakService extends BaseEntity { @Column({ type: "varchar", length: 255, nullable: false }) icon: string; + // @Column({ type: "int", nullable: false }) + // count: number; + + // @Column({ type: "float", nullable: false }) + // averageRating: number; + //------------------------------------- @ManyToOne(() => DanakServiceCategory, (danakServiceCategory) => danakServiceCategory.danakServices, { nullable: false, @@ -75,10 +81,4 @@ export class DanakService extends BaseEntity { @OneToMany(() => DanakServiceReview, (danakServiceReview) => danakServiceReview.service) reviews: DanakServiceReview[]; - - // @ManyToMany(() => Discount, (discount) => discount.services, { nullable: true }) - // discounts?: Discount[]; } - -// @ManyToMany(() => User, (user) => user.danakServices) -// users: User[]; diff --git a/src/modules/danak-services/providers/danak-services.service.ts b/src/modules/danak-services/providers/danak-services.service.ts index a923658..cd8c340 100755 --- a/src/modules/danak-services/providers/danak-services.service.ts +++ b/src/modules/danak-services/providers/danak-services.service.ts @@ -1,15 +1,15 @@ import { BadRequestException, Injectable } from "@nestjs/common"; -import Decimal from "decimal.js"; import { FindOptionsWhere, In, IsNull } from "typeorm"; import { ParamDto } from "../../../common/DTO/param.dto"; -import { CategoryMessage, CommonMessage, ServiceMessage } from "../../../common/enums/message.enum"; -import { DiscountCalculationType } from "../../discounts/enums/discount-type.enum"; +import { CategoryMessage, CommonMessage, ServiceMessage, SubscriptionMessage } from "../../../common/enums/message.enum"; +import { SubscriptionStatus } from "../../subscriptions/enums/subscription-status.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 { DanakServiceReviewQueryDto } from "../DTO/danak-service-review-query.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"; @@ -178,58 +178,97 @@ export class DanakServicesService { } /******************************************** */ - async getDanakServiceByIdWithSubs(serviceId: string, isAdmin: boolean) { - const danakService = await this.danakServicesRepository.findOne({ - where: { id: serviceId, ...(isAdmin && { isActive: true, subscriptionPlans: { isActive: true } }) }, - relations: { images: true, subscriptionPlans: { discounts: true } }, - }); + async getDanakServiceByIdWithSubs(serviceId: string, isAdmin: boolean, userId: string) { + const serviceQueryBuilder = this.danakServicesRepository + .createQueryBuilder("service") + .leftJoinAndSelect("service.images", "images") + .leftJoinAndSelect("service.subscriptionPlans", "subscriptionPlans") + .leftJoin("service.reviews", "reviews", "reviews.status = :reviewStatus", { reviewStatus: ReviewStatus.APPROVED }) + .leftJoin("reviews.user", "user") + .addSelect([ + "reviews.id", + "reviews.comment", + "reviews.rating", + "reviews.title", + "reviews.createdAt", + "reviews.status", + "user.id", + "user.firstName", + "user.lastName", + "user.profilePic", + ]) + // .loadRelationCountAndMap("service.reviewCount", "service.reviews") + // .addSelect( + // (subQuery) => + // subQuery + // .select("CAST(AVG(rating) AS DECIMAL(10,2))", "averageRating") + // .from(DanakServiceReview, "r") + // .where("r.serviceId = service.id") + // .andWhere("r.status = :reviewStatus", { reviewStatus: ReviewStatus.APPROVED }), + // "averageRating", + // ) + .where("service.id = :serviceId", { serviceId }) + .groupBy("service.id, images.id, subscriptionPlans.id, reviews.id, user.id"); + + if (!isAdmin) { + serviceQueryBuilder + .andWhere("service.isActive = :isActive", { isActive: true }) + .andWhere("subscriptionPlans.isActive = :planActive", { planActive: true }); + } + + const danakService = await serviceQueryBuilder.getOne(); if (!danakService) throw new BadRequestException(ServiceMessage.SERVICE_NOT_FOUND_BY_ID); - const transformedSubscriptions = danakService.subscriptionPlans.map((subscription) => { - let price = new Decimal(subscription.price); + const averageRatingResult = await this.danakServiceReviewRepository + .createQueryBuilder("r") + .select("CAST(AVG(r.rating) AS DECIMAL(10,2))", "averageRating") + .where("r.serviceId = :serviceId", { serviceId }) + .andWhere("r.status = :reviewStatus", { reviewStatus: ReviewStatus.APPROVED }) + .getRawOne(); - if (subscription.discounts && subscription.discounts.length > 0) { - subscription.discounts.forEach((discount) => { - if (discount.isActive && new Date() >= discount.startDate && new Date() <= discount.endDate) { - if (discount.calculationType === DiscountCalculationType.PERCENTAGE) { - const discountAmount = price.mul(discount.amount).div(100); - price = price.sub(discountAmount); - } else if (discount.calculationType === DiscountCalculationType.FIXED) { - price = price.sub(discount.amount); - } - } - }); - } + const averageRating = parseFloat(averageRatingResult?.averageRating) || 0; - return { - ...subscription, - finalPrice: price.toNumber(), - }; - }); + const hasSubscription = await this.checkUserPurchasedService(userId, serviceId); - danakService.subscriptionPlans = transformedSubscriptions; - - 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, + averageRating, + purchased: !!hasSubscription, + }; + } + /******************************************** */ + + async getDanakServiceReviews(queryDto: DanakServiceReviewQueryDto) { + const { limit, skip } = PaginationUtils(queryDto); + + const reviewsQueryBuilder = this.danakServiceReviewRepository + .createQueryBuilder("review") + .leftJoinAndSelect("review.user", "user") + .leftJoinAndSelect("review.service", "service") + .select([ + "review.id", + "review.title", + "review.comment", + "review.rating", + "review.status", + "review.createdAt", + "user.id", + "user.firstName", + "user.lastName", + "user.profilePic", + "service.id", + "service.name", + ]) + .orderBy("review.createdAt", "DESC") + .skip(skip) + .take(limit); + + const [reviews, count] = await reviewsQueryBuilder.getManyAndCount(); + + return { reviews, + count, + paginate: true, }; } @@ -261,6 +300,18 @@ export class DanakServicesService { async addReview(userId: string, serviceId: string, addReviewDto: AddReviewDto) { const service = await this.findServiceById(serviceId); + const hasSubscription = await this.checkUserPurchasedService(userId, serviceId); + if (!hasSubscription) throw new BadRequestException(SubscriptionMessage.NOT_PURCHASED_CANNOT_REVIEW); + + // // Check if user already reviewed this service + // const existingReview = await this.danakServiceReviewRepository.findOne({ + // where: { user: { id: userId }, service: { id: serviceId } }, + // }); + + // if (existingReview) { + // throw new BadRequestException(ServiceMessage.ALREADY_REVIEWED); + // } + const review = this.danakServiceReviewRepository.create({ user: { id: userId }, ...addReviewDto, service }); await this.danakServiceReviewRepository.save(review); return { @@ -311,6 +362,22 @@ export class DanakServicesService { return count; } - /******************************************** */ + + private async checkUserPurchasedService(userId: string, serviceId: string) { + const subscription = await this.danakServicesRepository + .createQueryBuilder() + .select("1") + .from("user_subscription", "us") + .innerJoin("subscription_plan", "sp", "us.planId = sp.id") + .where("us.userId = :userId", { userId }) + .andWhere("sp.serviceId = :serviceId", { serviceId }) + .andWhere(`(us.status = '${SubscriptionStatus.ACTIVE}')`) + .andWhere("us.endDate > NOW()") + .andWhere("us.startDate < NOW()") + .limit(1) + .getRawOne(); + + return !!subscription; + } } diff --git a/src/modules/discounts/entities/discount.entity.ts b/src/modules/discounts/entities/discount.entity.ts index 72f4aaa..ea982c1 100755 --- a/src/modules/discounts/entities/discount.entity.ts +++ b/src/modules/discounts/entities/discount.entity.ts @@ -36,7 +36,7 @@ export class Discount extends BaseEntity { @Column({ type: "timestamptz" }) endDate: Date; - @ManyToMany(() => SubscriptionPlan, (subscriptionPlan) => subscriptionPlan.discounts, { nullable: true }) + @ManyToMany(() => SubscriptionPlan, { nullable: true }) @JoinTable() subscriptionPlans: SubscriptionPlan[]; diff --git a/src/modules/invoices/providers/invoices.service.ts b/src/modules/invoices/providers/invoices.service.ts index 0ffee2e..f60608e 100755 --- a/src/modules/invoices/providers/invoices.service.ts +++ b/src/modules/invoices/providers/invoices.service.ts @@ -13,6 +13,7 @@ import { NotificationsService } from "../../notifications/providers/notification import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity"; import { UserSubscription } from "../../subscriptions/entities/user-subscription.entity"; import { SubscriptionStatus } from "../../subscriptions/enums/subscription-status.enum"; +import { User } from "../../users/entities/user.entity"; import { UsersService } from "../../users/providers/users.service"; import { OTPService } from "../../utils/providers/otp.service"; import { PaginationUtils } from "../../utils/providers/pagination.utils"; @@ -82,6 +83,7 @@ export class InvoicesService { { invoiceId: invoice.numericId.toString(), dueDate: invoice.dueDate, + createDate: invoice.createdAt, price: new Decimal(invoice.totalPrice).toNumber(), userPhone: user.phone, userEmail: user.email, @@ -173,7 +175,7 @@ export class InvoicesService { ///********************************** */ async createInvoiceForSubscription( - userId: string, + user: User, plan: SubscriptionPlan, userSub: UserSubscription, dueDate: Date, @@ -191,12 +193,26 @@ export class InvoicesService { ]; const invoice = queryRunner.manager.create(Invoice, { totalPrice: plan.price, - user: { id: userId }, - status: InvoiceStatus.PENDING, + user, + status: InvoiceStatus.WAIT_PAYMENT, paidAt: new Date(), dueDate, items: invoiceItems, }); + + await this.notificationsService.createInvoiceCreationNotification( + user.id, + { + invoiceId: invoice.numericId.toString(), + dueDate: invoice.dueDate, + createDate: invoice.createdAt, + price: new Decimal(invoice.totalPrice).toNumber(), + userPhone: user.phone, + userEmail: user.email, + items: invoiceItems.map((item) => item.name).join(", "), + }, + queryRunner, + ); // await queryRunner.manager.save(Invoice, invoice); return invoice; @@ -285,7 +301,6 @@ export class InvoicesService { const user = await this.usersService.findOneByIdWithQueryRunner(userId, queryRunner); const invoice = await this.getPendingInvoiceByIdWithQueryRunner(invoiceId, user.id, queryRunner); - console.log(invoice); if (invoice.status !== InvoiceStatus.WAIT_PAYMENT) throw new BadRequestException(InvoiceMessage.INVOICE_CAN_NOT_PAID); diff --git a/src/modules/notifications/interfaces/ISendNotificationData.ts b/src/modules/notifications/interfaces/ISendNotificationData.ts index 0268769..9b51d32 100644 --- a/src/modules/notifications/interfaces/ISendNotificationData.ts +++ b/src/modules/notifications/interfaces/ISendNotificationData.ts @@ -7,6 +7,7 @@ export interface IInvoiceNotificationData extends IBaseNotificationData { price: number; items: string; dueDate: Date; + createDate: Date; invoiceId: string; } diff --git a/src/modules/notifications/providers/notifications.service.ts b/src/modules/notifications/providers/notifications.service.ts index e8945c2..e3a4b62 100755 --- a/src/modules/notifications/providers/notifications.service.ts +++ b/src/modules/notifications/providers/notifications.service.ts @@ -137,7 +137,14 @@ export class NotificationsService { queryRunner, ); if (isActive) { - await this.smsService.sendInvoiceCreationSms(data.userPhone, data.invoiceId, data.price, data.items, dateFormat(data.dueDate)); + await this.smsService.sendInvoiceCreationSms( + data.userPhone, + data.invoiceId, + data.price, + data.items, + dateFormat(data.createDate), + dateFormat(data.dueDate), + ); //send email too } return this.createNotification({ title: NotificationMessage.INVOICE_CREATION, message, recipientId }, queryRunner); diff --git a/src/modules/subscriptions/entities/subscription.entity.ts b/src/modules/subscriptions/entities/subscription.entity.ts index 428b066..7876b24 100755 --- a/src/modules/subscriptions/entities/subscription.entity.ts +++ b/src/modules/subscriptions/entities/subscription.entity.ts @@ -1,11 +1,11 @@ // eslint-disable-next-line import/no-named-as-default import Decimal from "decimal.js"; -import { Column, Entity, Index, ManyToMany, ManyToOne } from "typeorm"; +import { Column, Entity, Index, ManyToOne } from "typeorm"; import { BaseEntity } from "../../../common/entities/base.entity"; import { DecimalTransformer } from "../../../common/transformers/decimal.transformer"; import { DanakService } from "../../danak-services/entities/danak-service.entity"; -import { Discount } from "../../discounts/entities/discount.entity"; +// import { Discount } from "../../discounts/entities/discount.entity"; @Entity() @Index(["service", "name"], { unique: true }) @@ -25,6 +25,6 @@ export class SubscriptionPlan extends BaseEntity { @ManyToOne(() => DanakService, (danakService) => danakService.subscriptionPlans, { nullable: false, onDelete: "CASCADE" }) service: DanakService; - @ManyToMany(() => Discount, (discount) => discount.subscriptionPlans) - discounts: Discount[]; + // @ManyToMany(() => Discount, (discount) => discount.subscriptionPlans) + // discounts: Discount[]; } diff --git a/src/modules/subscriptions/providers/subscriptions.service.ts b/src/modules/subscriptions/providers/subscriptions.service.ts index d56961b..e43dae5 100755 --- a/src/modules/subscriptions/providers/subscriptions.service.ts +++ b/src/modules/subscriptions/providers/subscriptions.service.ts @@ -1,17 +1,12 @@ import { BadRequestException, Injectable } from "@nestjs/common"; import dayjs from "dayjs"; -// eslint-disable-next-line import/no-named-as-default -import Decimal from "decimal.js"; import { DataSource, In } from "typeorm"; import { ServiceMessage, SubscriptionMessage } from "../../../common/enums/message.enum"; import { DanakServicesService } from "../../danak-services/providers/danak-services.service"; -import { DiscountCalculationType } from "../../discounts/enums/discount-type.enum"; import { InvoicesService } from "../../invoices/providers/invoices.service"; import { UsersService } from "../../users/providers/users.service"; import { PaginationUtils } from "../../utils/providers/pagination.utils"; -// import { Wallet } from "../../wallets/entities/wallet.entity"; -// import { WalletsService } from "../../wallets/providers/wallets.service"; import { AddSubscriptionsToServiceDto } from "../DTO/create-subscription.dto"; import { SearchUserSubsQueryDto } from "../DTO/search-user-subs-query.dto"; import { ServiceSubsQueryDto } from "../DTO/service-subs-query.dto"; @@ -30,7 +25,6 @@ export class SubscriptionsService { private readonly userSubscriptionsRepository: UserSubscriptionsRepository, private readonly invoicesService: InvoicesService, private readonly usersService: UsersService, - // private readonly walletsService: WalletsService, private readonly danakServices: DanakServicesService, private readonly dataSource: DataSource, ) {} @@ -162,31 +156,9 @@ export class SubscriptionsService { const [subscriptions, count] = await queryBuilder.skip(skip).take(limit).getManyAndCount(); - const transformedSubscriptions = subscriptions.map((subscription) => { - let price = new Decimal(subscription.price); - - if (subscription.discounts && subscription.discounts.length > 0) { - subscription.discounts.forEach((discount) => { - if (discount.isActive && new Date() >= discount.startDate && new Date() <= discount.endDate) { - if (discount.calculationType === DiscountCalculationType.PERCENTAGE) { - const discountAmount = price.mul(discount.amount).div(100); - price = price.sub(discountAmount); - } else if (discount.calculationType === DiscountCalculationType.FIXED) { - price = price.sub(discount.amount); - } - } - }); - } - - return { - ...subscription, - finalPrice: price.toNumber(), - }; - }); - return { service, - subscriptions: transformedSubscriptions, + subscriptions, count, pagination: true, }; @@ -219,7 +191,6 @@ export class SubscriptionsService { where: { id: subscribeDto.planId, service: { id: serviceId } }, relations: { service: true, - discounts: true, }, }); @@ -237,7 +208,7 @@ export class SubscriptionsService { //TODO: need queue handling for notification const invoiceDueDate = dayjs(userSubscription.startDate).add(5, "day").toDate(); - const invoice = await this.invoicesService.createInvoiceForSubscription(userId, plan, userSubscription, invoiceDueDate, queryRunner); + const invoice = await this.invoicesService.createInvoiceForSubscription(user, plan, userSubscription, invoiceDueDate, queryRunner); userSubscription.status = SubscriptionStatus.INACTIVE; await queryRunner.manager.save(UserSubscription, userSubscription); diff --git a/src/modules/utils/interfaces/ISms.ts b/src/modules/utils/interfaces/ISms.ts index f0d20e6..89744a3 100755 --- a/src/modules/utils/interfaces/ISms.ts +++ b/src/modules/utils/interfaces/ISms.ts @@ -39,5 +39,6 @@ export type TemplateParams = | "amount" | "newBalance" | "reason" + | "dueDate" | "subject" | "message"; diff --git a/src/modules/utils/providers/sms.service.ts b/src/modules/utils/providers/sms.service.ts index 919d54f..8090987 100755 --- a/src/modules/utils/providers/sms.service.ts +++ b/src/modules/utils/providers/sms.service.ts @@ -114,13 +114,14 @@ export class SmsService { } //************************************************* */ - async sendInvoiceCreationSms(mobile: string, invoiceId: string, price: number, items: string, invoiceDate: string) { + async sendInvoiceCreationSms(mobile: string, invoiceId: string, price: number, items: string, invoiceDate: string, dueDate: string) { const smsData: ISmsVerifyBody = { Parameters: [ { name: "invoiceId", value: invoiceId }, { name: "price", value: Intl.NumberFormat("fa-IR").format(price) }, { name: "items", value: items }, { name: "invoiceDate", value: invoiceDate }, + { name: "dueDate", value: dueDate }, ], Mobile: mobile, TemplateId: this.smsConfigs.SMS_PATTERN_INVOICE_CREATED,