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 { 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 { CreateDanakServiceCategoryDto } from "../DTO/create-category.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 { UpdateDanakServiceCategoryDto } from "../DTO/update-category.dto"; import { UpdateReviewStatusDto } from "../DTO/update-review-status.dto"; import { UpdateServiceDto } from "../DTO/update-service.dto"; import { DanakServiceCategory } from "../entities/danak-service-category.entity"; import { DanakServiceImage } from "../entities/danak-service-image.entity"; import { ReviewStatus } from "../enums/review-status.enum"; 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"; @Injectable() export class DanakServicesService { private readonly logger = new Logger(DanakServicesService.name); constructor( private readonly danakServicesRepository: DanakServicesRepository, private readonly danakServicesCategoryRepository: DanakServicesCategoryRepository, private readonly danakServiceReviewRepository: DanakServiceReviewRepository, private readonly danakServicesImageRepository: DanakServicesImageRepository, private readonly dataSource: DataSource, ) {} /******************************************** */ async createCategory(createDto: CreateDanakServiceCategoryDto) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { const existCategory = await queryRunner.manager.findOne(this.danakServicesCategoryRepository.target, { where: { title: createDto.title }, }); if (existCategory) { throw new BadRequestException(CategoryMessage.TITLE_EXIST); } const category = queryRunner.manager.create(this.danakServicesCategoryRepository.target, createDto); await queryRunner.manager.save(this.danakServicesCategoryRepository.target, category); await this.handleOrderCollision(queryRunner, category); await queryRunner.commitTransaction(); return { message: CommonMessage.CREATED, category, }; } catch (error) { await queryRunner.rollbackTransaction(); this.logger.error("Error in create category", error); throw new BadRequestException(CategoryMessage.CREATION_FAILED); } finally { await queryRunner.release(); } } //*********************************** */ private async handleOrderCollision(queryRunner: QueryRunner, newCategory: DanakServiceCategory): Promise { const conflictingCategory = await queryRunner.manager.findOne(this.danakServicesCategoryRepository.target, { where: { order: newCategory.order, id: Not(newCategory.id), deletedAt: IsNull(), }, }); if (conflictingCategory) { const highestOrderCategory = await queryRunner.manager.findOne(this.danakServicesCategoryRepository.target, { order: { order: "DESC" }, where: { deletedAt: IsNull() }, }); const newOrder = highestOrderCategory ? highestOrderCategory.order + 1 : newCategory.order + 1; await queryRunner.manager.update(this.danakServicesCategoryRepository.target, conflictingCategory.id, { order: newOrder }); } } /******************************************** */ async getCategoryById(categoryId: string) { const category = await this.danakServicesCategoryRepository.findOneById(categoryId); if (!category) throw new BadRequestException(CategoryMessage.CATEGORY_NOT_EXIST); return { category, }; } /******************************************** */ async updateCategory(categoryId: string, updateDto: UpdateDanakServiceCategoryDto) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { const category = await queryRunner.manager.findOne(DanakServiceCategory, { where: { id: categoryId } }); if (!category) throw new BadRequestException(CategoryMessage.CATEGORY_NOT_EXIST); if (updateDto.title) { const existCategory = await queryRunner.manager.findOne(DanakServiceCategory, { where: { title: updateDto.title, id: Not(categoryId) }, }); if (existCategory) throw new BadRequestException(CategoryMessage.TITLE_EXIST); } await queryRunner.manager.update(DanakServiceCategory, categoryId, updateDto); if (updateDto.order !== undefined && updateDto.order !== category.order) { await this.handleOrderCollision(queryRunner, { ...category, ...updateDto }); } await queryRunner.commitTransaction(); return { message: CommonMessage.UPDATE_SUCCESS, }; } catch (error) { await queryRunner.rollbackTransaction(); this.logger.error("Error in update category", error); throw new BadRequestException(CategoryMessage.UPDATE_FAILED); } finally { await queryRunner.release(); } } /******************************************** */ async deleteCategoryById(categoryId: string) { const category = await this.danakServicesCategoryRepository.findOneById(categoryId); if (!category) throw new BadRequestException(CategoryMessage.CATEGORY_NOT_EXIST); // await this.danakServicesCategoryRepository.softDelete(categoryId); return { message: CommonMessage.DELETED, }; } /******************************************** */ async getCategories(queryDto: CategorySearchQueryDto) { const where: FindOptionsWhere = { isActive: true, deletedAt: IsNull(), }; if (queryDto.parentId === undefined) { where.parentId = IsNull(); } else { // If parentId is provided, use it where.parentId = queryDto.parentId; } const categories = await this.danakServicesCategoryRepository.find({ where, order: { createdAt: "ASC" }, }); return { categories }; } /******************************************** */ async getCategoryList(queryDto: CategoryListSearchQueryDto) { const { limit, skip } = PaginationUtils(queryDto); const queryBuilder = this.danakServicesCategoryRepository .createQueryBuilder("category") .where("category.deletedAt IS NULL") .loadRelationCountAndMap("category.servicesCount", "category.danakServices") .loadRelationCountAndMap("category.childrenCount", "category.children"); if (queryDto.isActive !== undefined) { queryBuilder.andWhere("category.isActive = :isActive", { isActive: queryDto.isActive === 1 }); } if (queryDto.q) { queryBuilder.andWhere("category.title ILIKE :q", { q: `%${queryDto.q}%` }); } queryBuilder .leftJoinAndSelect("category.parent", "parent") .select(["category", "parent.id", "parent.title"]) .orderBy("category.createdAt", "DESC"); const [categories, count] = await queryBuilder.skip(skip).take(limit).getManyAndCount(); return { categories, count, paginate: true }; } /******************************************** */ async getCategoriesUserSide() { const categories = await this.danakServicesCategoryRepository.find({ where: { isActive: true, deletedAt: IsNull() }, order: { order: "ASC" }, }); return { categories, }; } /******************************************** */ async getCategoryServices(categoryId?: string) { const category = await this.danakServicesCategoryRepository.findOne({ where: { id: categoryId, isActive: true, deletedAt: IsNull(), danakServices: { isActive: true } }, relations: { danakServices: true, }, order: { createdAt: "DESC" }, }); if (!category) throw new BadRequestException(CategoryMessage.CATEGORY_NOT_EXIST_OR_HAS_NO_SERVICE); return { category, }; } /******************************************** */ async toggleCategoryStatus(paramDto: ParamDto) { const category = await this.danakServicesCategoryRepository.findOneById(paramDto.id); if (!category) throw new BadRequestException(CategoryMessage.CATEGORY_NOT_EXIST); // category.isActive = !category.isActive; // await this.danakServicesCategoryRepository.save(category); // return { message: CommonMessage.UPDATE_SUCCESS, isActive: category.isActive, }; } /******************************************** */ async toggleServiceStatus(paramDto: ParamDto) { const service = await this.danakServicesRepository.findServiceById(paramDto.id); if (!service) throw new BadRequestException(ServiceMessage.SERVICE_NOT_EXIST); // service.isActive = !service.isActive; // await this.danakServicesRepository.save(service); // return { message: CommonMessage.UPDATE_SUCCESS, isActive: service.isActive, }; } /******************************************** */ async toggleServiceSlider(paramDto: ParamDto) { const service = await this.danakServicesRepository.findServiceById(paramDto.id); if (!service) throw new BadRequestException(ServiceMessage.SERVICE_NOT_EXIST); // service.showInSlider = !service.showInSlider; // await this.danakServicesRepository.save(service); // return { message: CommonMessage.UPDATE_SUCCESS, showInSlider: service.showInSlider, }; } /******************************************** */ async createService(createDto: CreateServiceDto) { const category = await this.danakServicesCategoryRepository.findOneById(createDto.categoryId); if (!category) throw new BadRequestException(CategoryMessage.CATEGORY_NOT_EXIST); const existService = await this.danakServicesRepository.findOneByName(createDto.name); if (existService) throw new BadRequestException(ServiceMessage.NAME_EXIST); const images = createDto.images.map((image) => { return { imageUrl: image }; }); const service = this.danakServicesRepository.create({ ...createDto, category, images }); await this.danakServicesRepository.save(service); return { message: CommonMessage.CREATED, service, }; } /******************************************** */ async updateDanakService(serviceId: string, updateDto: UpdateServiceDto) { const service = await this.findServiceById(serviceId); let images: DanakServiceImage[] = []; if (updateDto.categoryId) { const category = await this.danakServicesCategoryRepository.findOneById(updateDto.categoryId); if (!category) throw new BadRequestException(CategoryMessage.CATEGORY_NOT_EXIST); service.category = category; } // if (updateDto.name) { const existService = await this.danakServicesRepository.findOneByName(updateDto.name, serviceId); if (existService) throw new BadRequestException(ServiceMessage.NAME_EXIST); } // if (updateDto.images) { await this.danakServicesImageRepository.delete({ danakService: { id: serviceId } }); const newImages = updateDto.images.map((image) => { return this.danakServicesImageRepository.create({ imageUrl: image, danakService: service, }); }); await this.danakServicesImageRepository.save(newImages); images = newImages; } await this.danakServicesRepository.save({ ...service, ...updateDto, images: images.length ? images : service.images }); return { message: CommonMessage.UPDATE_SUCCESS, service, }; } /******************************************** */ async deleteServiceById(serviceId: string) { await this.findServiceById(serviceId); await this.danakServicesRepository.softDelete(serviceId); return { message: CommonMessage.DELETED, }; } /******************************************** */ async getServicesList(queryDto: DanakServicesSearchQueryDto) { return this.danakServicesRepository.getServicesForAdmin(queryDto); } /******************************************** */ async getServicesListForUser(queryDto: DanakServicesQueryDto) { return this.danakServicesRepository.getServicesForUser(queryDto); } /******************************************** */ async searchServices(queryDto: DanakServicesGlobalSearchDto) { const services = await this.danakServicesRepository.searchServices(queryDto); return { services, }; } /******************************************** */ async getDanakSuggestServices() { const danakServices = await this.danakServicesRepository.find({ where: { isDanakSuggest: true, isActive: true, deletedAt: IsNull(), category: { isActive: true, deletedAt: IsNull() } }, relations: { category: true }, select: { id: true, name: true, title: true, description: true, createDate: true, icon: true, createdAt: true, isActive: true, isDanakSuggest: true, metaDescription: true, category: { id: true, title: true, }, }, order: { createdAt: "DESC" }, withDeleted: false, }); return { danakServices, }; } /******************************************** */ async getDanakServicesSlider() { const danakServices = await this.danakServicesRepository.find({ where: { showInSlider: true, isActive: true, deletedAt: IsNull(), category: { isActive: true, deletedAt: IsNull() } }, relations: { category: true, images: true }, select: { id: true, name: true, title: true, description: true, createDate: true, icon: true, createdAt: true, isActive: true, isDanakSuggest: true, showInSlider: true, metaDescription: true, category: { id: true, title: true, }, images: { id: true, imageUrl: true, }, }, order: { createdAt: "DESC" }, withDeleted: false, }); return { danakServices }; } /******************************************** */ async getDanakServiceByIdWithSubs(serviceId: string, isAdmin: boolean, userId: string) { const danakService = await this.danakServicesRepository.getDanakServiceById(serviceId, isAdmin); if (!danakService) throw new BadRequestException(ServiceMessage.SERVICE_NOT_FOUND_BY_ID); const averageRating = await this.danakServiceReviewRepository.getAverageRating(danakService.id); const hasSubscription = await this.checkUserPurchasedService(userId, serviceId); return { danakService, averageRating, purchased: !!hasSubscription, }; } /******************************************** */ async getDanakServiceByIdPublic(serviceId: string) { const danakService = await this.danakServicesRepository.getDanakServiceById(serviceId, false); if (!danakService) throw new BadRequestException(ServiceMessage.SERVICE_NOT_FOUND_BY_ID); const averageRating = await this.danakServiceReviewRepository.getAverageRating(danakService.id); return { danakService, averageRating }; } /******************************************** */ async getServiceUsers(serviceId: string) { await this.findServiceById(serviceId); const users = await this.danakServicesRepository.getUsersByService(serviceId); return { users }; } /******************************************** */ 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); return danakService; } /******************************************** */ async findServiceById(id: string) { const service = await this.danakServicesRepository.findOne({ where: { id, deletedAt: IsNull() }, relations: { images: true } }); if (!service) throw new BadRequestException(ServiceMessage.SERVICE_NOT_EXIST); return service; } /******************************************** */ async findServicesByIds(ids: string[]) { const services = await this.danakServicesRepository.findBy({ id: In(ids), deletedAt: IsNull() }); return services; } /******************************************** */ 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 { message: ServiceMessage.REVIEW_ADDED, review, }; } /******************************************** */ 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: { isActive: true, deletedAt: IsNull(), }, }); 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; } }