From 817d638f3e9412a46035891680b8e355d95b5724 Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Sat, 15 Feb 2025 12:08:19 +0330 Subject: [PATCH] chore: complete ads service --- package.json | 1 + pnpm-lock.yaml | 3 + src/app.module.ts | 2 + src/common/enums/message.enum.ts | 31 +++ src/modules/ads/DTO/ads-search-query.dto.ts | 32 +++ src/modules/ads/DTO/create-ads.dto.ts | 53 +++++ src/modules/ads/DTO/update-ads.dto.ts | 5 + src/modules/ads/ads.controller.ts | 74 +++++++ src/modules/ads/ads.module.ts | 16 ++ src/modules/ads/entities/ads.entity.ts | 32 +++ src/modules/ads/enums/ads-location.enum.ts | 8 + src/modules/ads/providers/ads.service.ts | 196 ++++++++++++++++++ .../ads/repositories/ads.repository.ts | 12 ++ .../DTO/create-announcement.dto.ts | 5 +- .../entities/danak-service.entity.ts | 8 + .../providers/danak-services.service.ts | 2 +- 16 files changed, 475 insertions(+), 5 deletions(-) create mode 100644 src/modules/ads/DTO/ads-search-query.dto.ts create mode 100644 src/modules/ads/DTO/create-ads.dto.ts create mode 100644 src/modules/ads/DTO/update-ads.dto.ts create mode 100644 src/modules/ads/ads.controller.ts create mode 100644 src/modules/ads/ads.module.ts create mode 100644 src/modules/ads/entities/ads.entity.ts create mode 100644 src/modules/ads/enums/ads-location.enum.ts create mode 100644 src/modules/ads/providers/ads.service.ts create mode 100644 src/modules/ads/repositories/ads.repository.ts diff --git a/package.json b/package.json index 92fe201..a182ad1 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "cache-manager": "^6.3.2", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "dayjs": "^1.11.13", "decimal.js": "^10.5.0", "dotenv": "^16.4.7", "fastify": "^5.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43ae138..ecc4cdc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,6 +86,9 @@ importers: class-validator: specifier: ^0.14.1 version: 0.14.1 + dayjs: + specifier: ^1.11.13 + version: 1.11.13 decimal.js: specifier: ^10.5.0 version: 10.5.0 diff --git a/src/app.module.ts b/src/app.module.ts index 3344b30..7d4e308 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -14,6 +14,7 @@ import { mailerConfig } from "./configs/mailer.config"; import { rateLimitConfig } from "./configs/rateLimit.config"; import { databaseConfigs } from "./configs/typeorm.config"; import { HTTPLogger } from "./core/middlewares/logger.middleware"; +import { AdsModule } from "./modules/ads/ads.module"; import { AnnouncementsModule } from "./modules/announcements/announcement.module"; import { AuthModule } from "./modules/auth/auth.module"; import { ContactUsModule } from "./modules/contact-us/contact-us.module"; @@ -57,6 +58,7 @@ import { WalletsModule } from "./modules/wallets/wallets.module"; SubscriptionsModule, LearningModule, DiscountModule, + AdsModule, ], controllers: [], providers: [], diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 1b3b6c4..a82ba92 100644 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -126,6 +126,7 @@ export const enum ServiceMessage { NAME_EXIST = "سرویس با این نام قبلا ثبت شده است", SERVICE_NOT_FOUND_BY_ID = "سرویسی با این شناسه یافت نشد یا سرویس غیرفعال می‌باشد", SERVICE_ID_SHOULD_BE_A_UUID = "شناسه سرویس باید یک UUID باشد", + SERVICE_ID_REQUIRED = "شناسه سرویس مورد نیاز است", } export const enum AnnouncementMessage { @@ -371,3 +372,33 @@ export const enum FinancialMessage { USER_ID_SHOULD_BE_A_UUID = "شناسه کاربر باید یک UUID معتبر باشد", FINANCIAL_INFO_ALREADY_EXISTS = "دیتا اطلاعات مالی وجود دارد", } +export const enum AdsMessage { + TITLE_REQUIRED = "عنوان الزامی است", + TITLE_STRING = "عنوان باید یک رشته باشد", + IMAGE_SHOULD_BE_URL = "آدرس تصویر باید یک URL معتبر باشد", + LINK_SHOULD_BE_URL = "لینک باید یک URL معتبر باشد", + DISPLAY_LOCATION_REQUIRED = "موقعیت نمایش الزامی است", + TITLE_LENGTH = "طول عنوان باید بین ۳ تا ۵۰ کاراکتر باشد", + INVALID_DISPLAY_LOCATION = "موقعیت نمایش نامعتبر است", + START_DATE_REQUIRED = "تاریخ شروع الزامی است", + INVALID_START_DATE = "تاریخ شروع نامعتبر است", + END_DATE_REQUIRED = "تاریخ پایان الزامی است", + INVALID_END_DATE = "تاریخ پایان نامعتبر است", + IS_ACTIVE_REQUIRED = "وضعیت فعال بودن الزامی است", + IS_ACTIVE_MUST_BE_BOOLEAN = "وضعیت فعال بودن باید true یا false باشد", + IMAGE_URL_REQUIRED = "آدرس تصویر الزامی است", + LINK_REQUIRED = "لینک الزامی است", + ADS_EXIST_WITH_TITLE = "تبلیغی با این عنوان وجود دارد", + START_DATE_MUST_BE_BEFORE_END_DATE = "تاریخ شروع باید قبل از تاریخ پایان باشد", + END_DATE_MUST_BE_AFTER_NOW = "تاریخ پایان باید بعد از تاریخ فعلی باشد", + ADS_CREATED = "تبلیغ با موفقیت ایجاد شد", + ADS_NOT_FOUND_BY_ID = "تبلیغی با این شناسه یافت نشد", + ADS_UPDATED = "تبلیغ با موفقیت به روز رسانی شد", + SEARCH_QUERY_STRING = "رشته جستجو باید یک رشته باشد", + START_DATE_MUST_BE_AFTER_NOW = "تاریخ شروع باید بعد از تاریخ فعلی باشد", + INVALID_DATE = "تاریخ نامعتبر است", + SERVICE_ADS_MUST_BE_DISPLAYED_ON_SERVICE_PAGE = "تبلیغات سرویس باید در خود سرویس نمایش داده شود", + ADS_DELETED = "تبلیغ با موفقیت حذف شد", + ADS_ACTIVATED = "تبلیغ با موفقیت فعال شد", + ADS_DEACTIVATED = "تبلیغ با موفقیت غیرفعال شد", +} diff --git a/src/modules/ads/DTO/ads-search-query.dto.ts b/src/modules/ads/DTO/ads-search-query.dto.ts new file mode 100644 index 0000000..91e8e72 --- /dev/null +++ b/src/modules/ads/DTO/ads-search-query.dto.ts @@ -0,0 +1,32 @@ +import { ApiPropertyOptional } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsDateString, IsEnum, IsOptional, IsString } from "class-validator"; + +import { PaginationDto } from "../../../common/DTO/pagination.dto"; +import { AdsMessage } from "../../../common/enums/message.enum"; +import { AdsDisplayLocation } from "../enums/ads-location.enum"; + +export class AdsSearchQueryDto extends PaginationDto { + @IsOptional() + @Type(() => Number) + // @IsBoolean({ message: CategoryMessage.IS_ACTIVE_SHOULD_BE_BOOLEAN }) + @ApiPropertyOptional({ description: "ads status", example: 1 }) + isActive?: number; + + @IsOptional() + @IsString({ message: AdsMessage.SEARCH_QUERY_STRING }) + @ApiPropertyOptional({ description: "Search query", example: "search query" }) + q?: string; + + @IsOptional() + @IsDateString({}, { message: AdsMessage.INVALID_DATE }) + @ApiPropertyOptional({ description: "since query", example: "2025-01-27" }) + since?: string; +} + +export class GetAdsByLocationQueryDto { + @IsOptional() + @IsEnum(AdsDisplayLocation) + @ApiPropertyOptional({ description: "Ads display location", example: AdsDisplayLocation.OTHER_SERVICES_LEFT }) + location?: AdsDisplayLocation; +} diff --git a/src/modules/ads/DTO/create-ads.dto.ts b/src/modules/ads/DTO/create-ads.dto.ts new file mode 100644 index 0000000..c363b60 --- /dev/null +++ b/src/modules/ads/DTO/create-ads.dto.ts @@ -0,0 +1,53 @@ +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { IsBoolean, IsDateString, IsEnum, IsNotEmpty, IsOptional, IsString, IsUUID, IsUrl, Length } from "class-validator"; + +import { AdsMessage, ServiceMessage } from "../../../common/enums/message.enum"; +import { AdsDisplayLocation } from "../enums/ads-location.enum"; + +export class CreateAdsDto { + @IsNotEmpty({ message: AdsMessage.TITLE_REQUIRED }) + @IsString({ message: AdsMessage.TITLE_STRING }) + @Length(3, 150, { message: AdsMessage.TITLE_LENGTH }) + @ApiProperty({ description: "The title of the ad", example: "New product ads" }) + title: string; + + @IsNotEmpty({ message: AdsMessage.DISPLAY_LOCATION_REQUIRED }) + @IsEnum(AdsDisplayLocation) + @ApiProperty({ + description: "The location where the ad will be displayed", + example: AdsDisplayLocation.HOMEPAGE_TOP, + enum: AdsDisplayLocation, + }) + displayLocation: AdsDisplayLocation; + + @IsNotEmpty({ message: AdsMessage.START_DATE_REQUIRED }) + @IsDateString({}, { message: AdsMessage.INVALID_START_DATE }) + @ApiProperty({ description: "The date when the ad will start displaying", example: "2022-01-01T00:00:00.000Z" }) + startDate: Date; + + @IsNotEmpty({ message: AdsMessage.END_DATE_REQUIRED }) + @IsDateString({}, { message: AdsMessage.INVALID_END_DATE }) + @ApiProperty({ description: "The date when the ad will stop displaying", example: "2022-01-31T23:59:59.999Z" }) + endDate: Date; + + @IsNotEmpty({ message: AdsMessage.IS_ACTIVE_REQUIRED }) + @IsBoolean({ message: AdsMessage.IS_ACTIVE_MUST_BE_BOOLEAN }) + @ApiProperty({ description: "The status of the ad", example: true }) + isActive: boolean; + + @IsNotEmpty({ message: AdsMessage.IMAGE_URL_REQUIRED }) + @IsUrl({ protocols: ["http", "https"], require_protocol: true }, { message: AdsMessage.IMAGE_SHOULD_BE_URL }) + @ApiProperty({ description: "The URL of the image to be displayed", example: "https://test.com/pic.png" }) + imageUrl: string; + + @IsNotEmpty({ message: AdsMessage.LINK_REQUIRED }) + @IsUrl({ protocols: ["http", "https"], require_protocol: true }, { message: AdsMessage.LINK_SHOULD_BE_URL }) + @ApiProperty({ description: "The URL to redirect to when the ad is clicked", example: "https://test.com" }) + link: string; + + @IsOptional() + @IsNotEmpty({ message: ServiceMessage.SERVICE_ID_REQUIRED }) + @IsUUID("4", { message: ServiceMessage.SERVICE_ID_SHOULD_BE_A_UUID }) + @ApiPropertyOptional({ description: "Service ID", example: "d290f1ee-6c54-4b01-90e6-d701748f0851" }) + serviceId: string; +} diff --git a/src/modules/ads/DTO/update-ads.dto.ts b/src/modules/ads/DTO/update-ads.dto.ts new file mode 100644 index 0000000..07e1be4 --- /dev/null +++ b/src/modules/ads/DTO/update-ads.dto.ts @@ -0,0 +1,5 @@ +import { PartialType } from "@nestjs/swagger"; + +import { CreateAdsDto } from "./create-ads.dto"; + +export class UpdateAdsDto extends PartialType(CreateAdsDto) {} diff --git a/src/modules/ads/ads.controller.ts b/src/modules/ads/ads.controller.ts new file mode 100644 index 0000000..52c4545 --- /dev/null +++ b/src/modules/ads/ads.controller.ts @@ -0,0 +1,74 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from "@nestjs/common"; +import { ApiOperation, ApiTags } from "@nestjs/swagger"; + +import { AdsSearchQueryDto, GetAdsByLocationQueryDto } from "./DTO/ads-search-query.dto"; +import { CreateAdsDto } from "./DTO/create-ads.dto"; +import { UpdateAdsDto } from "./DTO/update-ads.dto"; +import { AdsService } from "./providers/ads.service"; +import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; +import { Roles } from "../../common/decorators/roles.decorator"; +import { ParamDto } from "../../common/DTO/param.dto"; +import { RoleEnum } from "../users/enums/role.enum"; + +@Controller("advertise") +@ApiTags("Advertisements") +@AuthGuards() +export class AdsController { + constructor(private readonly adsService: AdsService) {} + + @ApiOperation({ summary: "Create new ads" }) + @Roles(RoleEnum.ADMIN) + @Post() + createAds(@Body() createDto: CreateAdsDto) { + return this.adsService.createAds(createDto); + } + + @ApiOperation({ summary: "Get all ads" }) + @Roles(RoleEnum.ADMIN) + @Get("list") + getAdsList(@Query() queryDto: AdsSearchQueryDto) { + return this.adsService.getAdsList(queryDto); + } + + @ApiOperation({ summary: "Get ads by id" }) + @Roles(RoleEnum.ADMIN) + @Get(":id") + getAdsById(@Param() paramDto: ParamDto) { + return this.adsService.getAdsById(paramDto.id); + } + + @ApiOperation({ summary: "Update ads by id" }) + @Roles(RoleEnum.ADMIN) + @Patch(":id") + updateAdsById(@Param() paramDto: ParamDto, @Body() updateDto: UpdateAdsDto) { + return this.adsService.updateAdsById(paramDto.id, updateDto); + } + + @ApiOperation({ summary: "Delete ads by id" }) + @Roles(RoleEnum.ADMIN) + @Delete(":id") + deleteAdsById(@Param() paramDto: ParamDto) { + return this.adsService.deleteAdsById(paramDto.id); + } + + @ApiOperation({ summary: "Toggle ads status" }) + @Roles(RoleEnum.ADMIN) + @Patch("toggle-status/:id") + toggleAdsStatus(@Param() paramDto: ParamDto) { + return this.adsService.toggleAdsStatus(paramDto.id); + } + + @ApiOperation({ summary: "Get ads by location ==> both user and admin" }) + @Roles(RoleEnum.ADMIN, RoleEnum.USER) + @Get("by-location") + getAdsByLocation(@Query() queryDto: GetAdsByLocationQueryDto) { + return this.adsService.getAdsByLocation(queryDto); + } + + @ApiOperation({ summary: "Get all ads grouped by location ==> both user and admin" }) + @Roles(RoleEnum.ADMIN, RoleEnum.USER) + @Get("grouped-by-location") + getAdsGroupedByLocation() { + return this.adsService.getAdsGroupedByLocation(); + } +} diff --git a/src/modules/ads/ads.module.ts b/src/modules/ads/ads.module.ts new file mode 100644 index 0000000..b53e8f6 --- /dev/null +++ b/src/modules/ads/ads.module.ts @@ -0,0 +1,16 @@ +import { Module } from "@nestjs/common"; +import { TypeOrmModule } from "@nestjs/typeorm"; + +import { AdsController } from "./ads.controller"; +import { Ads } from "./entities/ads.entity"; +import { AdsService } from "./providers/ads.service"; +import { AdsRepository } from "./repositories/ads.repository"; +import { DanakServicesModule } from "../danak-services/danak-services.module"; + +@Module({ + imports: [TypeOrmModule.forFeature([Ads]), DanakServicesModule], + controllers: [AdsController], + providers: [AdsService, AdsRepository], + exports: [AdsService], +}) +export class AdsModule {} diff --git a/src/modules/ads/entities/ads.entity.ts b/src/modules/ads/entities/ads.entity.ts new file mode 100644 index 0000000..8c92da5 --- /dev/null +++ b/src/modules/ads/entities/ads.entity.ts @@ -0,0 +1,32 @@ +import { Column, Entity, ManyToOne } from "typeorm"; + +import { BaseEntity } from "../../../common/entities/base.entity"; +import { DanakService } from "../../danak-services/entities/danak-service.entity"; +import { AdsDisplayLocation } from "../enums/ads-location.enum"; + +@Entity() +export class Ads extends BaseEntity { + @Column({ type: "varchar", length: 150, unique: true }) + title: string; + + @Column({ type: "enum", enum: AdsDisplayLocation, nullable: false }) + displayLocation: AdsDisplayLocation; + + @Column({ type: "timestamptz", nullable: false }) + startDate: Date; + + @Column({ type: "timestamptz", nullable: false }) + endDate: Date; + + @Column({ type: "boolean", default: true }) + isActive: boolean; + + @Column({ type: "varchar", length: 250, nullable: false }) + imageUrl: string; + + @Column({ type: "varchar", length: 250, nullable: false }) + link: string; + + @ManyToOne(() => DanakService, (danakService) => danakService.ads, { nullable: true, onDelete: "SET NULL" }) + service: DanakService | null; +} diff --git a/src/modules/ads/enums/ads-location.enum.ts b/src/modules/ads/enums/ads-location.enum.ts new file mode 100644 index 0000000..4f1a506 --- /dev/null +++ b/src/modules/ads/enums/ads-location.enum.ts @@ -0,0 +1,8 @@ +export enum AdsDisplayLocation { + HOMEPAGE_TOP = "HOMEPAGE_TOP", + SINGLE_SERVICE_PAGE = "SINGLE_SERVICE_PAGE", + MY_SERVICES_PAGE = "MY_SERVICES_PAGE", + OTHER_SERVICES_TOP = "OTHER_SERVICES_TOP", + OTHER_SERVICES_LEFT = "OTHER_SERVICES_LEFT", + SERVICE = "SERVICE", +} diff --git a/src/modules/ads/providers/ads.service.ts b/src/modules/ads/providers/ads.service.ts new file mode 100644 index 0000000..cac30c6 --- /dev/null +++ b/src/modules/ads/providers/ads.service.ts @@ -0,0 +1,196 @@ +import { BadRequestException, Injectable } from "@nestjs/common"; +import dayjs from "dayjs"; +import { FindOptionsWhere, LessThanOrEqual, MoreThan, Not } from "typeorm"; + +import { AdsMessage, ServiceMessage } from "../../../common/enums/message.enum"; +import { DanakServicesService } from "../../danak-services/providers/danak-services.service"; +import { PaginationUtils } from "../../utils/providers/pagination.utils"; +import { AdsSearchQueryDto, GetAdsByLocationQueryDto } from "../DTO/ads-search-query.dto"; +import { CreateAdsDto } from "../DTO/create-ads.dto"; +import { UpdateAdsDto } from "../DTO/update-ads.dto"; +import { Ads } from "../entities/ads.entity"; +import { AdsDisplayLocation } from "../enums/ads-location.enum"; +import { AdsRepository } from "../repositories/ads.repository"; + +@Injectable() +export class AdsService { + constructor( + private readonly adsRepository: AdsRepository, + private readonly danakServicesService: DanakServicesService, + ) {} + // + async createAds(createDto: CreateAdsDto) { + const existAds = await this.adsRepository.findOneBy({ title: createDto.title }); + if (existAds) throw new BadRequestException(AdsMessage.ADS_EXIST_WITH_TITLE); + + let danakService = null; + + if (createDto.serviceId) { + danakService = await this.danakServicesService.findServiceById(createDto.serviceId); + if (!danakService) throw new BadRequestException(ServiceMessage.SERVICE_NOT_FOUND_BY_ID); + if (createDto.displayLocation !== AdsDisplayLocation.SERVICE) + throw new BadRequestException(AdsMessage.SERVICE_ADS_MUST_BE_DISPLAYED_ON_SERVICE_PAGE); + } + + this.validateDates(createDto.startDate, createDto.endDate); + + const ads = this.adsRepository.create({ ...createDto, service: danakService }); + + await this.adsRepository.save(ads); + // + return { + message: AdsMessage.ADS_CREATED, + ads, + }; + } + + //***************************************** */ + async getAdsById(adsId: string) { + const ads = await this.findAdsById(adsId); + return { + ads, + }; + } + + //***************************************** */ + async getAdsGroupedByLocation() { + const now = new Date(); + + const adsGroups = await this.adsRepository + .createQueryBuilder("ads") + .select(["ads.displayLocation", "jsonb_agg(ads.*) AS ads"]) + .where("ads.isActive = :isActive", { isActive: true }) + .andWhere("ads.startDate <= :now", { now }) + .andWhere("ads.endDate > :now", { now }) + .groupBy("ads.displayLocation") + .getRawMany(); + + return { adsGroups }; + } + //***************************************** */ + async updateAdsById(adsId: string, updateDto: UpdateAdsDto) { + const ads = await this.findAdsById(adsId); + + if (updateDto.title) { + const existAds = await this.adsRepository.findOneBy({ title: updateDto.title, id: Not(adsId) }); + if (existAds) throw new BadRequestException(AdsMessage.ADS_EXIST_WITH_TITLE); + } + + if (updateDto.startDate && updateDto.endDate) { + this.validateDates(updateDto.startDate, updateDto.endDate); + } + + let danakService = null; + + if (updateDto.serviceId) { + danakService = await this.danakServicesService.findServiceById(updateDto.serviceId); + if (!danakService) throw new BadRequestException(ServiceMessage.SERVICE_NOT_FOUND_BY_ID); + } + + await this.adsRepository.save({ ...ads, ...updateDto, service: danakService }); + return { + message: AdsMessage.ADS_UPDATED, + }; + } + //****************************************** */ + async getAdsByLocation(queryDto: GetAdsByLocationQueryDto) { + const whereClause: FindOptionsWhere = { + isActive: true, + startDate: LessThanOrEqual(new Date()), + endDate: MoreThan(new Date()), + }; + + if (queryDto.location) { + whereClause.displayLocation = queryDto.location; + } + + const ads = await this.adsRepository.find({ + where: whereClause, + }); + return { ads }; + } + + //***************************************** */ + + async deleteAdsById(adsId: string) { + const ads = await this.findAdsById(adsId); + await this.adsRepository.remove(ads); + return { + message: AdsMessage.ADS_DELETED, + }; + } + //***************************************** */ + + async toggleAdsStatus(adsId: string) { + const ads = await this.findAdsById(adsId); + ads.isActive = !ads.isActive; + // + await this.adsRepository.save(ads); + return { + message: ads.isActive ? AdsMessage.ADS_ACTIVATED : AdsMessage.ADS_DEACTIVATED, + isActive: ads.isActive, + }; + } + + //***************************************** */ + + async getAdsList(queryDto: AdsSearchQueryDto) { + const { limit, skip } = PaginationUtils(queryDto); + + const queryBuilder = this.adsRepository.createQueryBuilder("ads"); + queryBuilder.leftJoinAndSelect("ads.service", "service", "service.id IS NOT NULL"); + + if (queryDto.isActive !== undefined) { + queryBuilder.andWhere("ads.isActive = :isActive", { isActive: queryDto.isActive === 1 }); + } + + if (queryDto.q) { + queryBuilder.andWhere("ads.title ILIKE :q", { q: `%${queryDto.q}%` }); + } + + if (queryDto.since) { + queryBuilder.andWhere("(ads.createdAt >= :since OR ads.startDate >= :since OR ads.endDate >= :since)", { + since: new Date(queryDto.since), + }); + } + + queryBuilder.orderBy("ads.createdAt", "DESC").skip(skip).take(limit); + + const [ads, count] = await queryBuilder.getManyAndCount(); + + return { + ads, + count, + paginate: true, + }; + } + + //***************************************** */ + async findAdsById(adsId: string) { + const ads = await this.adsRepository.findOne({ where: { id: adsId }, relations: { service: true } }); + if (!ads) throw new BadRequestException(AdsMessage.ADS_NOT_FOUND_BY_ID); + return ads; + } + + //***************************************** */ + + private validateDates(startDate: Date, endDate: Date) { + if (dayjs(startDate).isAfter(dayjs(endDate))) { + throw new BadRequestException(AdsMessage.START_DATE_MUST_BE_BEFORE_END_DATE); + } + + if (dayjs(startDate).isBefore(dayjs())) { + throw new BadRequestException(AdsMessage.START_DATE_MUST_BE_AFTER_NOW); + } + + if (dayjs(startDate).isSame(dayjs(endDate))) { + throw new BadRequestException(AdsMessage.START_DATE_MUST_BE_BEFORE_END_DATE); + } + + if (dayjs(endDate).isBefore(dayjs())) { + throw new BadRequestException(AdsMessage.END_DATE_MUST_BE_AFTER_NOW); + } + } + + //***************************************** */ +} diff --git a/src/modules/ads/repositories/ads.repository.ts b/src/modules/ads/repositories/ads.repository.ts new file mode 100644 index 0000000..98344db --- /dev/null +++ b/src/modules/ads/repositories/ads.repository.ts @@ -0,0 +1,12 @@ +import { Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; + +import { Ads } from "../entities/ads.entity"; + +@Injectable() +export class AdsRepository extends Repository { + constructor(@InjectRepository(Ads) adsRepository: Repository) { + super(adsRepository.target, adsRepository.manager, adsRepository.queryRunner); + } +} diff --git a/src/modules/announcements/DTO/create-announcement.dto.ts b/src/modules/announcements/DTO/create-announcement.dto.ts index 4aeea1a..3af4513 100644 --- a/src/modules/announcements/DTO/create-announcement.dto.ts +++ b/src/modules/announcements/DTO/create-announcement.dto.ts @@ -46,10 +46,7 @@ export class CreateAnnouncementDto { @IsOptional() @IsNotEmpty({ message: AnnouncementMessage.SERVICE_MUST_BE_ID }) @IsUUID("4", { message: AnnouncementMessage.SERVICE_MUST_BE_UUID }) - @ApiProperty({ - description: "Service ID", - example: "d290f1ee-6c54-4b01-90e6-d701748f0851", - }) + @ApiProperty({ description: "Service ID", example: "d290f1ee-6c54-4b01-90e6-d701748f0851" }) serviceId: string; @IsOptional() diff --git a/src/modules/danak-services/entities/danak-service.entity.ts b/src/modules/danak-services/entities/danak-service.entity.ts index 90f333b..7b86eb3 100644 --- a/src/modules/danak-services/entities/danak-service.entity.ts +++ b/src/modules/danak-services/entities/danak-service.entity.ts @@ -3,6 +3,7 @@ import { Column, Entity, ManyToOne, OneToMany } from "typeorm"; import { DanakServiceCategory } from "./danak-service-category.entity"; import { DanakServiceImage } from "./danak-service-image.entity"; import { BaseEntity } from "../../../common/entities/base.entity"; +import { Ads } from "../../ads/entities/ads.entity"; import { Announcement } from "../../announcements/entities/announcement.entity"; import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity"; import { Ticket } from "../../tickets/entities/ticket.entity"; @@ -49,6 +50,7 @@ export class DanakService extends BaseEntity { @Column({ type: "varchar", length: 255, nullable: false }) icon: string; + //------------------------------------- @ManyToOne(() => DanakServiceCategory, (danakServiceCategory) => danakServiceCategory.danakServices, { nullable: false, onDelete: "RESTRICT", @@ -66,6 +68,12 @@ export class DanakService extends BaseEntity { @OneToMany(() => SubscriptionPlan, (plan) => plan.service, { cascade: true }) subscriptionPlans: SubscriptionPlan[]; + + @OneToMany(() => Ads, (ads) => ads.service) + ads: Ads[]; + + // @ManyToMany(() => Discount, (discount) => discount.services, { nullable: true }) + // discounts?: Discount[]; } // @ManyToMany(() => User, (user) => user.danakServices) diff --git a/src/modules/danak-services/providers/danak-services.service.ts b/src/modules/danak-services/providers/danak-services.service.ts index 832069c..d8bd5a0 100644 --- a/src/modules/danak-services/providers/danak-services.service.ts +++ b/src/modules/danak-services/providers/danak-services.service.ts @@ -66,7 +66,7 @@ export class DanakServicesService { } if (queryDto.q) { - queryBuilder.andWhere("category.title LIKE :q", { q: `%${queryDto.q}%` }); + queryBuilder.andWhere("category.title ILIKE :q", { q: `%${queryDto.q}%` }); } queryBuilder