From 0764c2689feefcf293077ea903a0e0915476dab3 Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Tue, 19 Aug 2025 13:15:13 +0330 Subject: [PATCH] feat: enhance danak services with feedback system and improved metadata tracking - Add service feedback system with CRUD operations - Add service guide functionality - Enhance metadata tracking with comprehensive headers (user-agent, referer, language, etc.) - Add new DTOs for feedback and guide management - Add repositories for feedback and guide entities - Update controller with new endpoints for feedback management - Improve service organization and method structure - Add payment and user entity enhancements --- src/common/enums/message.enum.ts | 11 ++ .../DTO/create-service-feedback.dto.ts | 28 ++++ .../DTO/create-service-guide.dto.ts | 39 +++++ .../DTO/service-feedback-query.dto.ts | 17 ++ .../DTO/service-guide-query.dto.ts | 18 +++ .../DTO/update-feedback-status.dto.ts | 18 +++ .../DTO/update-service-guide.dto.ts | 5 + .../danak-services.controller.ts | 70 +++++++- .../danak-services/danak-services.module.ts | 15 +- .../entities/danak-service-feedback.entity.ts | 26 +++ .../entities/danak-service-guide.entity.ts | 28 ++++ .../entities/danak-service.entity.ts | 8 + .../providers/danak-services.service.ts | 150 ++++++++++++++++++ .../danak-service-feedback.repository.ts | 41 +++++ .../danak-service-guide.repository.ts | 40 +++++ .../entities/deposit-request.entity.ts | 2 +- .../payments/providers/payments.service.ts | 7 +- src/modules/users/entities/user.entity.ts | 4 + 18 files changed, 511 insertions(+), 16 deletions(-) create mode 100644 src/modules/danak-services/DTO/create-service-feedback.dto.ts create mode 100644 src/modules/danak-services/DTO/create-service-guide.dto.ts create mode 100644 src/modules/danak-services/DTO/service-feedback-query.dto.ts create mode 100644 src/modules/danak-services/DTO/service-guide-query.dto.ts create mode 100644 src/modules/danak-services/DTO/update-feedback-status.dto.ts create mode 100644 src/modules/danak-services/DTO/update-service-guide.dto.ts create mode 100644 src/modules/danak-services/entities/danak-service-feedback.entity.ts create mode 100644 src/modules/danak-services/entities/danak-service-guide.entity.ts create mode 100644 src/modules/danak-services/repositories/danak-service-feedback.repository.ts create mode 100644 src/modules/danak-services/repositories/danak-service-guide.repository.ts diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 30aa721..dfe2d2d 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -212,6 +212,17 @@ export const enum ServiceMessage { PAGE_DESCRIPTION_REQUIRED = "توضیحات صفحه سرویس مورد نیاز است", PAGE_DESCRIPTION_STRING = "توضیحات صفحه سرویس باید یک رشته باشد", PAGE_DESCRIPTION_LENGTH = "توضیحات صفحه سرویس باید بین ۱۰ تا ۵۰۰ کاراکتر باشد", + SERVICE_GUIDE_NOT_EXIST = "راهنمای سرویس مورد نظر یافت نشد", + GUIDE_TITLE_STRING = "عنوان راهنمای سرویس باید یک رشته باشد", + GUIDE_TITLE_REQUIRED = "عنوان راهنمای سرویس مورد نیاز است", + GUIDE_DESCRIPTION_STRING = "توضیحات راهنمای سرویس باید یک رشته باشد", + GUIDE_DESCRIPTION_REQUIRED = "توضیحات راهنمای سرویس مورد نیاز است", + GUIDE_CONTENT_STRING = "متن راهنمای سرویس باید یک رشته باشد", + GUIDE_CONTENT_REQUIRED = "متن راهنمای سرویس مورد نیاز است", + GUIDE_ORDER_IS_REQUIRED = "ترتیب راهنمای سرویس مورد نیاز است", + GUIDE_ORDER_IS_NUMBER = "ترتیب راهنمای سرویس باید یک عدد صحیح باشد", + GUIDE_IS_ACTIVE_BOOLEAN = "وضعیت راهنمای سرویس باید یک بولین باشد", + GUIDE_TITLE_LENGTH = "عنوان راهنمای سرویس باید بین ۳ تا ۲۰۰ کاراکتر باشد", } export const enum AnnouncementMessage { diff --git a/src/modules/danak-services/DTO/create-service-feedback.dto.ts b/src/modules/danak-services/DTO/create-service-feedback.dto.ts new file mode 100644 index 0000000..981ff99 --- /dev/null +++ b/src/modules/danak-services/DTO/create-service-feedback.dto.ts @@ -0,0 +1,28 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsIn, IsInt, IsNotEmpty, IsString, IsUUID, Length } from "class-validator"; + +import { ServiceMessage, UserMessage } from "../../../common/enums/message.enum"; + +export class CreateServiceFeedbackDto { + @IsNotEmpty({ message: ServiceMessage.COMMENT_REQUIRED }) + @IsString({ message: ServiceMessage.COMMENT_STRING }) + @Length(1, 1000, { message: ServiceMessage.COMMENT_LENGTH }) + @ApiProperty({ description: "Feedback content", example: "This service works perfectly for my needs" }) + content: string; + + @IsNotEmpty({ message: ServiceMessage.RATING_REQUIRED }) + @IsInt({ message: ServiceMessage.RATING_INT }) + @IsIn([1, 2, 3, 4, 5], { message: ServiceMessage.RATING_RANGE }) + @ApiProperty({ description: "Rating (1-5, only for reviews)", example: 5, required: false }) + rating: number; + + @IsUUID("4", { message: ServiceMessage.SERVICE_ID_SHOULD_BE_A_UUID }) + @IsNotEmpty({ message: ServiceMessage.SERVICE_ID_REQUIRED }) + @ApiProperty({ description: "Service ID", example: "d290f1ee-6c54-4b01-90e6-d701748f0851" }) + serviceId: string; + + @IsNotEmpty({ message: UserMessage.USER_ID_REQUIRED }) + @IsUUID("all", { message: UserMessage.USER_ID_SHOULD_BE_A_UUID }) + @ApiProperty({ description: "User ID", example: "d290f1ee-6c54-4b01-90e6-d701748f0851" }) + userId: string; +} diff --git a/src/modules/danak-services/DTO/create-service-guide.dto.ts b/src/modules/danak-services/DTO/create-service-guide.dto.ts new file mode 100644 index 0000000..86800b5 --- /dev/null +++ b/src/modules/danak-services/DTO/create-service-guide.dto.ts @@ -0,0 +1,39 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsBoolean, IsNotEmpty, IsNumber, IsOptional, IsString, IsUUID, MaxLength } from "class-validator"; + +import { ServiceMessage } from "../../../common/enums/message.enum"; + +export class CreateServiceGuideDto { + @IsString({ message: ServiceMessage.GUIDE_TITLE_STRING }) + @IsNotEmpty({ message: ServiceMessage.GUIDE_TITLE_REQUIRED }) + @MaxLength(200, { message: ServiceMessage.GUIDE_TITLE_LENGTH }) + @ApiProperty({ description: "Guide title", example: "Getting Started with Service X" }) + title: string; + + @IsString({ message: ServiceMessage.GUIDE_DESCRIPTION_STRING }) + @IsNotEmpty({ message: ServiceMessage.GUIDE_DESCRIPTION_REQUIRED }) + @MaxLength(200) + @ApiProperty({ description: "Guide description", example: "This guide will help you get started with Service X" }) + description: string; + + @IsString({ message: ServiceMessage.GUIDE_CONTENT_STRING }) + @IsNotEmpty({ message: ServiceMessage.GUIDE_CONTENT_REQUIRED }) + @ApiProperty({ description: "Guide content in markdown or HTML", example: "# Getting Started\n\nThis guide will help you..." }) + content: string; + + @IsOptional() + @IsNotEmpty({ message: ServiceMessage.GUIDE_ORDER_IS_REQUIRED }) + @IsNumber({ allowInfinity: false, allowNaN: false }, { message: ServiceMessage.GUIDE_ORDER_IS_NUMBER }) + @ApiProperty({ description: "Order for displaying guides", example: 1 }) + order?: number; + + @IsOptional() + @IsBoolean({ message: ServiceMessage.GUIDE_IS_ACTIVE_BOOLEAN }) + @ApiProperty({ description: "Whether the guide is active", example: true, required: false }) + isActive?: boolean; + + @IsNotEmpty({ message: ServiceMessage.SERVICE_ID_REQUIRED }) + @IsUUID("4", { message: ServiceMessage.SERVICE_ID_SHOULD_BE_A_UUID }) + @ApiProperty({ description: "Service ID this guide belongs to" }) + serviceId: string; +} diff --git a/src/modules/danak-services/DTO/service-feedback-query.dto.ts b/src/modules/danak-services/DTO/service-feedback-query.dto.ts new file mode 100644 index 0000000..a179f7b --- /dev/null +++ b/src/modules/danak-services/DTO/service-feedback-query.dto.ts @@ -0,0 +1,17 @@ +import { ApiPropertyOptional } from "@nestjs/swagger"; +import { IsOptional, IsString, IsUUID } from "class-validator"; + +import { PaginationDto } from "../../../common/DTO/pagination.dto"; +import { CommonMessage, ServiceMessage } from "../../../common/enums/message.enum"; + +export class ServiceFeedbackQueryDto extends PaginationDto { + @IsOptional() + @IsUUID("4", { message: ServiceMessage.SERVICE_ID_SHOULD_BE_A_UUID }) + @ApiPropertyOptional({ description: "Service ID", example: "d290f1ee-6c54-4b01-90e6-d701748f0851" }) + serviceId: string; + + @IsOptional() + @IsString({ message: CommonMessage.SEARCH_QUERY_STRING }) + @ApiPropertyOptional({ description: "Search term for content", example: "search query" }) + q?: string; +} diff --git a/src/modules/danak-services/DTO/service-guide-query.dto.ts b/src/modules/danak-services/DTO/service-guide-query.dto.ts new file mode 100644 index 0000000..f2dbfd3 --- /dev/null +++ b/src/modules/danak-services/DTO/service-guide-query.dto.ts @@ -0,0 +1,18 @@ +import { ApiPropertyOptional } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsIn, IsOptional, IsString } from "class-validator"; + +import { ServiceMessage } from "../../../common/enums/message.enum"; + +export class ServiceGuideQueryDto { + @IsOptional() + @IsString() + @ApiPropertyOptional({ description: "Search term for title and content", example: "search query" }) + q?: string; + + @IsOptional() + @Type(() => Number) + @IsIn([1, 0], { message: ServiceMessage.IS_ACTIVE_SHOULD_BE_1_0 }) + @ApiPropertyOptional({ description: "guide status", example: 1 }) + isActive?: number; +} diff --git a/src/modules/danak-services/DTO/update-feedback-status.dto.ts b/src/modules/danak-services/DTO/update-feedback-status.dto.ts new file mode 100644 index 0000000..aad4d45 --- /dev/null +++ b/src/modules/danak-services/DTO/update-feedback-status.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsEnum, IsUUID } from "class-validator"; + +import { ReviewStatus } from "../enums/review-status.enum"; + +export class UpdateFeedbackStatusDto { + @ApiProperty({ description: "Feedback ID" }) + @IsUUID() + id: string; + + @ApiProperty({ + description: "New status for the feedback", + enum: ReviewStatus, + example: ReviewStatus.APPROVED, + }) + @IsEnum(ReviewStatus) + status: ReviewStatus; +} diff --git a/src/modules/danak-services/DTO/update-service-guide.dto.ts b/src/modules/danak-services/DTO/update-service-guide.dto.ts new file mode 100644 index 0000000..558f5c2 --- /dev/null +++ b/src/modules/danak-services/DTO/update-service-guide.dto.ts @@ -0,0 +1,5 @@ +import { PartialType } from "@nestjs/swagger"; + +import { CreateServiceGuideDto } from "./create-service-guide.dto"; + +export class UpdateServiceGuideDto extends PartialType(CreateServiceGuideDto) {} diff --git a/src/modules/danak-services/danak-services.controller.ts b/src/modules/danak-services/danak-services.controller.ts index 159e8f9..96a3f3b 100755 --- a/src/modules/danak-services/danak-services.controller.ts +++ b/src/modules/danak-services/danak-services.controller.ts @@ -1,15 +1,20 @@ -import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Query } from "@nestjs/common"; +import { Body, Controller, Delete, Get, Headers, HttpCode, HttpStatus, Ip, Param, Patch, 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 { CreateDanakServiceCategoryDto } from "./DTO/create-category.dto"; +import { CreateServiceFeedbackDto } from "./DTO/create-service-feedback.dto"; +import { CreateServiceGuideDto } from "./DTO/create-service-guide.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 { GetServiceBySlugParamDto } from "./DTO/get-service-by-slug-param.dto"; +import { ServiceFeedbackQueryDto } from "./DTO/service-feedback-query.dto"; +import { ServiceGuideQueryDto } from "./DTO/service-guide-query.dto"; import { UpdateDanakServiceCategoryDto } from "./DTO/update-category.dto"; import { UpdateReviewStatusDto } from "./DTO/update-review-status.dto"; +import { UpdateServiceGuideDto } from "./DTO/update-service-guide.dto"; import { UpdateServiceDto } from "./DTO/update-service.dto"; import { DanakServicesService } from "./providers/danak-services.service"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; @@ -223,11 +228,60 @@ export class DanakServicesController { return this.danakServicesService.updateDanakServiceReviewStatus(updateParamDto); } - // @AuthGuards() - // - // @ApiOperation({ summary: "get all user purchased danak services" }) - // @Get("user-services") - // getUserServices(@UserDec() user: User) { - // return this.danakServicesService.getUserServices(user.id); - // } + //------------------------ service guides ------------------------ + @ApiOperation({ summary: "Create a service guide => admin route" }) + @AuthGuards() + @PermissionsDec(PermissionEnum.SERVICES) + @Post("guides") + createServiceGuide(@Body() createDto: CreateServiceGuideDto) { + return this.danakServicesService.createServiceGuide(createDto); + } + + @ApiOperation({ summary: "Get service guides => public route" }) + @Get(":id/guides") + getServiceGuides(@Param() paramDto: ParamDto, @Query() queryDto: ServiceGuideQueryDto) { + return this.danakServicesService.getServiceGuides(paramDto.id, queryDto); + } + + @ApiOperation({ summary: "Update service guide => admin route" }) + @AuthGuards() + @PermissionsDec(PermissionEnum.SERVICES) + @Patch("guides/:id") + updateServiceGuide(@Param() paramDto: ParamDto, @Body() updateDto: UpdateServiceGuideDto) { + return this.danakServicesService.updateServiceGuide(paramDto.id, updateDto); + } + + @ApiOperation({ summary: "Delete service guide => admin route" }) + @AuthGuards() + @PermissionsDec(PermissionEnum.SERVICES) + @Delete("guides/:id") + @HttpCode(HttpStatus.NO_CONTENT) + deleteServiceGuide(@Param() paramDto: ParamDto) { + return this.danakServicesService.deleteServiceGuide(paramDto.id); + } + + //------------------------ service feedback ------------------------ + @ApiOperation({ summary: "Create service feedback => user route" }) + @AuthGuards() + @Post("feedback") + createServiceFeedback(@Body() createDto: CreateServiceFeedbackDto, @Headers() headers: Record, @Ip() ip: string) { + return this.danakServicesService.createServiceFeedback(createDto, headers, ip); + } + + @ApiOperation({ summary: "Get service feedbacks => admin route" }) + @AuthGuards() + @PermissionsDec(PermissionEnum.SERVICES) + @Pagination() + @Get("feedback") + getServiceFeedbacks(@Query() queryDto: ServiceFeedbackQueryDto) { + return this.danakServicesService.getServicesFeedbacks(queryDto); + } + + @ApiOperation({ summary: "Get service feedback statistics => admin route" }) + @AuthGuards() + @PermissionsDec(PermissionEnum.SERVICES) + @Get(":id/feedback") + getServiceFeedbackStats(@Param() paramDto: ParamDto) { + return this.danakServicesService.getServiceFeedback(paramDto.id); + } } diff --git a/src/modules/danak-services/danak-services.module.ts b/src/modules/danak-services/danak-services.module.ts index c004fde..3cfa347 100755 --- a/src/modules/danak-services/danak-services.module.ts +++ b/src/modules/danak-services/danak-services.module.ts @@ -3,10 +3,14 @@ import { TypeOrmModule } from "@nestjs/typeorm"; import { DanakServicesController } from "./danak-services.controller"; import { DanakServiceCategory } from "./entities/danak-service-category.entity"; +import { DanakServiceFeedback } from "./entities/danak-service-feedback.entity"; +import { DanakServiceGuide } from "./entities/danak-service-guide.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 { DanakServiceFeedbackRepository } from "./repositories/danak-service-feedback.repository"; +import { DanakServiceGuideRepository } from "./repositories/danak-service-guide.repository"; import { DanakServicesCategoryRepository } from "./repositories/danak-services-category.repository"; import { DanakServicesImageRepository } from "./repositories/danak-services-image.repository"; import { DanakServiceReviewRepository } from "./repositories/danak-services-review.repository"; @@ -16,7 +20,14 @@ import { UsersModule } from "../users/users.module"; @Module({ imports: [ - TypeOrmModule.forFeature([DanakService, DanakServiceImage, DanakServiceCategory, DanakServiceReview]), + TypeOrmModule.forFeature([ + DanakService, + DanakServiceImage, + DanakServiceCategory, + DanakServiceReview, + DanakServiceFeedback, + DanakServiceGuide, + ]), UsersModule, NotificationModule, ], @@ -26,6 +37,8 @@ import { UsersModule } from "../users/users.module"; DanakServicesCategoryRepository, DanakServicesImageRepository, DanakServiceReviewRepository, + DanakServiceFeedbackRepository, + DanakServiceGuideRepository, ], controllers: [DanakServicesController], exports: [DanakServicesService], diff --git a/src/modules/danak-services/entities/danak-service-feedback.entity.ts b/src/modules/danak-services/entities/danak-service-feedback.entity.ts new file mode 100644 index 0000000..92f6b37 --- /dev/null +++ b/src/modules/danak-services/entities/danak-service-feedback.entity.ts @@ -0,0 +1,26 @@ +import { Column, DeleteDateColumn, Entity, ManyToOne } from "typeorm"; + +import { DanakService } from "./danak-service.entity"; +import { BaseEntity } from "../../../common/entities/base.entity"; +import { User } from "../../users/entities/user.entity"; + +@Entity() +export class DanakServiceFeedback extends BaseEntity { + @Column({ type: "text", nullable: false }) + content: string; + + @Column({ type: "int", nullable: false }) + rating: number; + + @Column({ type: "json", nullable: true }) + metadata: Record | null; + + @ManyToOne(() => User, (user) => user.serviceFeedbacks, { onDelete: "SET NULL", nullable: true }) + user: User | null; + + @ManyToOne(() => DanakService, (service) => service.feedbacks, { onDelete: "CASCADE", nullable: false }) + service: DanakService; + + @DeleteDateColumn({ nullable: true }) + deletedAt?: Date; +} diff --git a/src/modules/danak-services/entities/danak-service-guide.entity.ts b/src/modules/danak-services/entities/danak-service-guide.entity.ts new file mode 100644 index 0000000..bfc524f --- /dev/null +++ b/src/modules/danak-services/entities/danak-service-guide.entity.ts @@ -0,0 +1,28 @@ +import { Column, DeleteDateColumn, Entity, ManyToOne } from "typeorm"; + +import { DanakService } from "./danak-service.entity"; +import { BaseEntity } from "../../../common/entities/base.entity"; + +@Entity() +export class DanakServiceGuide extends BaseEntity { + @Column({ type: "varchar", length: 200, nullable: false }) + title: string; + + @Column({ type: "varchar", length: 200, nullable: false }) + description: string; + + @Column({ type: "text", nullable: false }) + content: string; + + @Column({ type: "int", nullable: false, default: 0 }) + order: number; + + @Column({ type: "boolean", default: true }) + isActive: boolean; + + @ManyToOne(() => DanakService, (service) => service.guides, { onDelete: "CASCADE", nullable: false }) + service: DanakService; + + @DeleteDateColumn({ type: "timestamptz" }) + deletedAt?: Date; +} diff --git a/src/modules/danak-services/entities/danak-service.entity.ts b/src/modules/danak-services/entities/danak-service.entity.ts index 9345f7b..cdcda89 100755 --- a/src/modules/danak-services/entities/danak-service.entity.ts +++ b/src/modules/danak-services/entities/danak-service.entity.ts @@ -1,6 +1,8 @@ import { Column, DeleteDateColumn, Entity, Index, ManyToOne, OneToMany } from "typeorm"; import { DanakServiceCategory } from "./danak-service-category.entity"; +import { DanakServiceFeedback } from "./danak-service-feedback.entity"; +import { DanakServiceGuide } from "./danak-service-guide.entity"; import { DanakServiceImage } from "./danak-service-image.entity"; import { DanakServiceReview } from "./danak-service-review.entity"; import { BaseEntity } from "../../../common/entities/base.entity"; @@ -92,6 +94,12 @@ export class DanakService extends BaseEntity { @OneToMany(() => DanakServiceReview, (danakServiceReview) => danakServiceReview.service) reviews: DanakServiceReview[]; + @OneToMany(() => DanakServiceFeedback, (feedback) => feedback.service, { cascade: true }) + feedbacks: DanakServiceFeedback[]; + + @OneToMany(() => DanakServiceGuide, (guide) => guide.service, { cascade: true }) + guides: DanakServiceGuide[]; + @DeleteDateColumn() deletedAt?: Date; } diff --git a/src/modules/danak-services/providers/danak-services.service.ts b/src/modules/danak-services/providers/danak-services.service.ts index 22203fb..7018cff 100755 --- a/src/modules/danak-services/providers/danak-services.service.ts +++ b/src/modules/danak-services/providers/danak-services.service.ts @@ -5,21 +5,29 @@ import { ParamDto } from "../../../common/DTO/param.dto"; import { CategoryMessage, CommonMessage, ServiceMessage, SubscriptionMessage } from "../../../common/enums/message.enum"; import { NotificationQueue } from "../../notifications/queue/notification.queue"; import { SubscriptionStatus } from "../../subscriptions/enums/subscription-status.enum"; +import { User } from "../../users/entities/user.entity"; import { AdminsService } from "../../users/providers/admins.service"; import { UsersService } from "../../users/providers/users.service"; 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 { CreateServiceFeedbackDto } from "../DTO/create-service-feedback.dto"; +import { CreateServiceGuideDto } from "../DTO/create-service-guide.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 { ServiceFeedbackQueryDto } from "../DTO/service-feedback-query.dto"; +import { ServiceGuideQueryDto } from "../DTO/service-guide-query.dto"; import { UpdateDanakServiceCategoryDto } from "../DTO/update-category.dto"; import { UpdateReviewStatusDto } from "../DTO/update-review-status.dto"; +import { UpdateServiceGuideDto } from "../DTO/update-service-guide.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 { DanakServiceFeedbackRepository } from "../repositories/danak-service-feedback.repository"; +import { DanakServiceGuideRepository } from "../repositories/danak-service-guide.repository"; import { DanakServicesCategoryRepository } from "../repositories/danak-services-category.repository"; import { DanakServicesImageRepository } from "../repositories/danak-services-image.repository"; import { DanakServiceReviewRepository } from "../repositories/danak-services-review.repository"; @@ -31,6 +39,8 @@ export class DanakServicesService { private readonly danakServicesRepository: DanakServicesRepository, private readonly danakServicesCategoryRepository: DanakServicesCategoryRepository, private readonly danakServiceReviewRepository: DanakServiceReviewRepository, + private readonly danakServiceFeedbackRepository: DanakServiceFeedbackRepository, + private readonly danakServiceGuideRepository: DanakServiceGuideRepository, private readonly danakServicesImageRepository: DanakServicesImageRepository, private readonly dataSource: DataSource, private readonly adminsService: AdminsService, @@ -601,6 +611,146 @@ export class DanakServicesService { return count; } + + /******************************************** */ + async createServiceGuide(createDto: CreateServiceGuideDto) { + const queryRunner = this.dataSource.createQueryRunner(); + + try { + await queryRunner.connect(); + await queryRunner.startTransaction(); + const service = await this.findServiceById(createDto.serviceId); + + const guide = queryRunner.manager.create(this.danakServiceGuideRepository.target, { + ...createDto, + service, + order: createDto.order || 0, + }); + + await queryRunner.manager.save(this.danakServiceGuideRepository.target, guide); + + await queryRunner.commitTransaction(); + + return { message: CommonMessage.CREATED, guide }; + } catch (error) { + await queryRunner.rollbackTransaction(); + this.logger.error("Error creating service guide", error); + throw error; + } finally { + await queryRunner.release(); + } + } + /******************************************** */ + async getServiceGuides(serviceId: string, queryDto: ServiceGuideQueryDto) { + const guides = await this.danakServiceGuideRepository.findServiceGuides(serviceId, queryDto); + + return { guides }; + } + + /******************************************** */ + + async updateServiceGuide(guideId: string, updateDto: UpdateServiceGuideDto) { + const guide = await this.danakServiceGuideRepository.findOneBy({ + id: guideId, + service: { id: updateDto.serviceId }, + deletedAt: IsNull(), + }); + + if (!guide) throw new BadRequestException(ServiceMessage.SERVICE_GUIDE_NOT_EXIST); + + await this.danakServiceGuideRepository.save({ ...guide, ...updateDto }); + + return { message: CommonMessage.UPDATED, guide }; + } + + /******************************************** */ + + async deleteServiceGuide(guideId: string) { + const guide = await this.danakServiceGuideRepository.findOneBy({ id: guideId, deletedAt: IsNull() }); + + if (!guide) throw new BadRequestException(ServiceMessage.SERVICE_GUIDE_NOT_EXIST); + + await this.danakServiceGuideRepository.softDelete(guideId); + + return { + message: CommonMessage.DELETED, + }; + } + + /******************************************** */ + + async createServiceFeedback(createDto: CreateServiceFeedbackDto, headers: Record, ip: string) { + const queryRunner = this.dataSource.createQueryRunner(); + + try { + await queryRunner.connect(); + await queryRunner.startTransaction(); + + const service = await queryRunner.manager.findOne(this.danakServicesRepository.target, { + where: { id: createDto.serviceId, deletedAt: IsNull() }, + }); + + if (!service) throw new BadRequestException(ServiceMessage.SERVICE_NOT_EXIST); + + const user = await queryRunner.manager.findOne(User, { where: { id: createDto.userId, deletedAt: IsNull() } }); + + const feedback = queryRunner.manager.create(this.danakServiceFeedbackRepository.target, { + ...createDto, + service, + user, + metadata: { + userIp: ip, + referer: headers["referer"] || headers["referrer"], + userAgent: headers["user-agent"], + acceptLanguage: headers["accept-language"], + acceptEncoding: headers["accept-encoding"], + contentType: headers["content-type"], + origin: headers["origin"], + host: headers["host"], + xForwardedFor: headers["x-forwarded-for"], + xRealIp: headers["x-real-ip"], + timestamp: new Date().toISOString(), + platform: headers["sec-ch-ua-platform"], + mobile: headers["sec-ch-ua-mobile"], + }, + }); + + await queryRunner.manager.save(this.danakServiceFeedbackRepository.target, feedback); + + await queryRunner.commitTransaction(); + + return { + message: CommonMessage.CREATED, + feedback, + }; + } catch (error) { + await queryRunner.rollbackTransaction(); + this.logger.error("Error creating service feedback", error); + throw error; + } finally { + await queryRunner.release(); + } + } + + /******************************************** */ + async getServicesFeedbacks(queryDto: ServiceFeedbackQueryDto) { + const [feedbacks, count] = await this.danakServiceFeedbackRepository.findServiceFeedbacks(queryDto); + return { feedbacks, count, paginate: true }; + } + + /******************************************** */ + + async getServiceFeedback(serviceId: string) { + const service = await this.findServiceById(serviceId); + + const feedback = await this.danakServiceFeedbackRepository.findByServiceId(serviceId); + + return { + service: { id: service.id, name: service.name, description: service.description, isActive: service.isActive }, + feedback, + }; + } + /******************************************** */ private generateSlug(slug: string): string { diff --git a/src/modules/danak-services/repositories/danak-service-feedback.repository.ts b/src/modules/danak-services/repositories/danak-service-feedback.repository.ts new file mode 100644 index 0000000..2306944 --- /dev/null +++ b/src/modules/danak-services/repositories/danak-service-feedback.repository.ts @@ -0,0 +1,41 @@ +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { FindOptionsWhere, IsNull, Repository } from "typeorm"; + +import { PaginationUtils } from "../../utils/providers/pagination.utils"; +import { ServiceFeedbackQueryDto } from "../DTO/service-feedback-query.dto"; +import { DanakServiceFeedback } from "../entities/danak-service-feedback.entity"; + +@Injectable() +export class DanakServiceFeedbackRepository extends Repository { + constructor(@InjectRepository(DanakServiceFeedback) repository: Repository) { + super(repository.target, repository.manager, repository.queryRunner); + } + + async findByServiceId(serviceId: string): Promise { + const where: FindOptionsWhere = { service: { id: serviceId }, deletedAt: IsNull() }; + + return this.find({ where, order: { createdAt: "DESC" }, relations: ["user", "service"] }); + } + /******************************************** */ + async findServiceFeedbacks(queryDto: ServiceFeedbackQueryDto) { + const { limit, skip } = PaginationUtils(queryDto); + + const queryBuilder = this.createQueryBuilder("feedback") + .leftJoinAndSelect("feedback.service", "service") + .leftJoinAndSelect("feedback.user", "user") + .where("feedback.deletedAt IS NULL"); + + if (queryDto.serviceId) { + queryBuilder.andWhere("feedback.serviceId = :serviceId", { serviceId: queryDto.serviceId }); + } + + if (queryDto.q) { + queryBuilder.andWhere("feedback.content ILIKE :search", { search: `%${queryDto.q}%` }); + } + + queryBuilder.orderBy("feedback.createdAt", "DESC").skip(skip).take(limit); + + return queryBuilder.getManyAndCount(); + } +} diff --git a/src/modules/danak-services/repositories/danak-service-guide.repository.ts b/src/modules/danak-services/repositories/danak-service-guide.repository.ts new file mode 100644 index 0000000..d9c03ea --- /dev/null +++ b/src/modules/danak-services/repositories/danak-service-guide.repository.ts @@ -0,0 +1,40 @@ +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; + +import { ServiceGuideQueryDto } from "../DTO/service-guide-query.dto"; +import { DanakServiceGuide } from "../entities/danak-service-guide.entity"; + +@Injectable() +export class DanakServiceGuideRepository extends Repository { + constructor(@InjectRepository(DanakServiceGuide) repository: Repository) { + super(repository.target, repository.manager, repository.queryRunner); + } + + async findByServiceId(serviceId: string): Promise { + return this.find({ + where: { service: { id: serviceId }, isActive: true }, + order: { order: "ASC", createdAt: "DESC" }, + }); + } + + async findServiceGuides(serviceId: string, queryDto: ServiceGuideQueryDto) { + const queryBuilder = this.createQueryBuilder("guide").leftJoinAndSelect("guide.service", "service").where("guide.deletedAt IS NULL"); + + queryBuilder.andWhere("guide.serviceId = :serviceId", { serviceId }); + + if (queryDto.q) { + queryBuilder.andWhere("(guide.title ILIKE :search OR guide.content ILIKE :search OR guide.description ILIKE :search)", { + search: `%${queryDto.q}%`, + }); + } + + if (queryDto.isActive !== undefined) { + queryBuilder.andWhere("guide.isActive = :isActive", { isActive: queryDto.isActive === 1 }); + } + + queryBuilder.orderBy("guide.order", "ASC").addOrderBy("guide.createdAt", "DESC"); + + return queryBuilder.getMany(); + } +} diff --git a/src/modules/payments/entities/deposit-request.entity.ts b/src/modules/payments/entities/deposit-request.entity.ts index a64ffb0..2d56ea2 100755 --- a/src/modules/payments/entities/deposit-request.entity.ts +++ b/src/modules/payments/entities/deposit-request.entity.ts @@ -21,7 +21,7 @@ export class DepositRequest extends BaseEntity { receiptUrl: string; @Column({ type: "varchar", length: 250, nullable: true }) - comment: string; + comment: string | null; @Column({ type: "enum", enum: TransferType, nullable: false }) type: TransferType; diff --git a/src/modules/payments/providers/payments.service.ts b/src/modules/payments/providers/payments.service.ts index 83b3681..11bccaf 100755 --- a/src/modules/payments/providers/payments.service.ts +++ b/src/modules/payments/providers/payments.service.ts @@ -622,12 +622,7 @@ export class PaymentsService { //=============================================== private async updateDepositRequestStatus(depositId: string, status: DepositRequestStatus, comment: string | null, qryRnr: QueryRunner) { - const updateData: Partial = { status }; - if (comment !== null) { - updateData.comment = comment; - } - - await qryRnr.manager.update(DepositRequest, { id: depositId }, updateData); + await qryRnr.manager.update(DepositRequest, { id: depositId }, { status, comment }); } //=============================================== private async addPaymentReminderToQueue(payment: Payment) { diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index fb960ed..7b097ed 100755 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -11,6 +11,7 @@ import { UserAnnouncement } from "../../announcements/entities/user-announcement import { BlogComment } from "../../blogs/entities/blog-comment.entity"; import { Blog } from "../../blogs/entities/blog.entity"; import { Criticism } from "../../criticisms/entities/criticism.entity"; +import { DanakServiceFeedback } from "../../danak-services/entities/danak-service-feedback.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"; @@ -81,6 +82,9 @@ export class User extends BaseEntity { @OneToMany(() => DanakServiceReview, (danakServiceReview) => danakServiceReview.user) reviews: DanakServiceReview[]; + @OneToMany(() => DanakServiceFeedback, (feedback) => feedback.user) + serviceFeedbacks: DanakServiceFeedback[]; + @OneToMany(() => Blog, (blog) => blog.author) blogs: Blog[];