diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 4af4d0c..2894fbe 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -516,6 +516,8 @@ export const enum DiscountMessage { MAX_USER_USAGE_REACHED = "شما قبلا از حداکثر تعداد مجاز این کد تخفیف استفاده کرده‌اید", NOT_APPLICABLE = "این کد تخفیف برای محصولات انتخاب شده معتبر نیست", VALUE_REQUIRED = "مقدار تخفیف الزامی است", + IS_ACTIVE_REQUIRED = "وضعیت فعال بودن الزامی است", + CODE_GENERATION_FAILED = "ایجاد کد تخفیف با خطا مواجه شد", } export const enum EmailMessage { diff --git a/src/configs/typeorm.config.ts b/src/configs/typeorm.config.ts index 9391ab2..8d8378f 100755 --- a/src/configs/typeorm.config.ts +++ b/src/configs/typeorm.config.ts @@ -13,7 +13,7 @@ export function databaseConfigs(): TypeOrmModuleAsyncOptions { username: configService.getOrThrow("DB_USER"), password: configService.getOrThrow("DB_PASS"), autoLoadEntities: true, - synchronize: configService.getOrThrow("NODE_ENV") == "production" ? false : false, + synchronize: configService.getOrThrow("NODE_ENV") == "production" ? false : true, logging: configService.getOrThrow("NODE_ENV") == "production" ? false : true, migrationsTableName: "typeorm_migrations", migrationsRun: false, diff --git a/src/modules/announcements/announcement.controller.ts b/src/modules/announcements/announcement.controller.ts index 21517da..66d44b5 100755 --- a/src/modules/announcements/announcement.controller.ts +++ b/src/modules/announcements/announcement.controller.ts @@ -55,4 +55,11 @@ export class AnnouncementController { getAnnouncementById(@Param() paramDto: ParamDto, @UserDec("id") userId: string) { return this.announcementService.getAnnouncementById(paramDto.id, userId); } + + @ApiProperty({ description: "Get one announcements with id (admin route)" }) + @PermissionsDec(PermissionEnum.ANNOUNCEMENTS) + @Get(":id/admin") + getAnnouncementByIdAdmin(@Param() paramDto: ParamDto) { + return this.announcementService.getAnnouncementByIdAdmin(paramDto.id); + } } diff --git a/src/modules/announcements/providers/announcement.service.ts b/src/modules/announcements/providers/announcement.service.ts index 7c154b4..99854a6 100755 --- a/src/modules/announcements/providers/announcement.service.ts +++ b/src/modules/announcements/providers/announcement.service.ts @@ -151,6 +151,16 @@ export class AnnouncementService { return { announcement }; } + /***************************************** */ + async getAnnouncementByIdAdmin(id: string) { + const announcement = await this.announcementRepository.findOne({ where: { id }, relations: { targetService: true } }); + if (!announcement) throw new BadRequestException(AnnouncementMessage.NOT_FOUND); + // + return { + announcement, + }; + } + /***************************************** */ async getAnnouncementByService(queryDto: SearchAnnouncementQueryDto, serviceId: string) { diff --git a/src/modules/danak-services/entities/danak-service.entity.ts b/src/modules/danak-services/entities/danak-service.entity.ts index 1ad5b84..dea052c 100755 --- a/src/modules/danak-services/entities/danak-service.entity.ts +++ b/src/modules/danak-services/entities/danak-service.entity.ts @@ -6,7 +6,6 @@ import { DanakServiceReview } from "./danak-service-review.entity"; import { BaseEntity } from "../../../common/entities/base.entity"; import { Ads } from "../../ads/entities/ads.entity"; import { Announcement } from "../../announcements/entities/announcement.entity"; -import { DiscountService } from "../../discounts/entities/discount-product.entity"; import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity"; import { Ticket } from "../../tickets/entities/ticket.entity"; import { ServicesLanguage } from "../enums/services-language.enum"; @@ -77,9 +76,6 @@ export class DanakService extends BaseEntity { @OneToMany(() => DanakServiceReview, (danakServiceReview) => danakServiceReview.service) reviews: DanakServiceReview[]; - @OneToMany(() => DiscountService, (discountService) => discountService.danakService) - discountServices: DiscountService[]; - @DeleteDateColumn() deletedAt?: Date; } diff --git a/src/modules/discounts/DTO/create-discount.dto.ts b/src/modules/discounts/DTO/create-discount.dto.ts index d8fd1db..6cb5a33 100644 --- a/src/modules/discounts/DTO/create-discount.dto.ts +++ b/src/modules/discounts/DTO/create-discount.dto.ts @@ -1,6 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsArray, IsBoolean, IsDateString, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, Min, ValidateIf } from "class-validator"; +import { IsArray, IsBoolean, IsDateString, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateIf } from "class-validator"; import { DiscountMessage } from "../../../common/enums/message.enum"; import { DiscountType } from "../enums/discount-type.enum"; @@ -39,25 +38,10 @@ export class CreateDiscountDto { @ApiProperty({ description: "The end date of the discount", example: "2023-06-30T23:59:59Z" }) endDate: string; - @IsOptional() + @IsNotEmpty({ message: DiscountMessage.IS_ACTIVE_REQUIRED }) @IsBoolean({ message: DiscountMessage.IS_ACTIVE_MUST_BE_A_BOOLEAN }) @ApiProperty({ description: "Indicates if the discount is active", example: true }) - isActive?: boolean; - - @IsOptional() - @IsNumber({}, { message: "Max usage must be a number" }) - @Min(0, { message: "Max usage must be at least 0" }) - @Type(() => Number) - @ApiProperty({ description: "Maximum number of times this discount can be used (null for unlimited)", example: 1000, required: false }) - maxUsage?: number; - - @IsOptional() - @IsNumber({}, { message: "Max usage per user must be a number" }) - @Min(0, { message: "Max usage per user must be at least 0" }) - @Type(() => Number) - @ValidateIf((o) => !o.direct) - @ApiPropertyOptional({ description: "Maximum number of times a single user can use this discount (null for unlimited)", example: 1 }) - maxUsagePerUser?: number; + isActive: boolean; @IsOptional() @IsNotEmpty({ message: DiscountMessage.TARGET_PRODUCTS_MUST_BE_AN_ARRAY }) diff --git a/src/modules/discounts/discounts.controller.ts b/src/modules/discounts/discounts.controller.ts index e8222e5..66a5725 100644 --- a/src/modules/discounts/discounts.controller.ts +++ b/src/modules/discounts/discounts.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, Param, Post } from "@nestjs/common"; +import { Body, Controller, Post } from "@nestjs/common"; import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; import { CreateDiscountDto } from "./DTO/create-discount.dto"; @@ -22,12 +22,4 @@ export class DiscountsController { create(@Body() createDiscountDto: CreateDiscountDto) { return this.discountsService.create(createDiscountDto); } - - @ApiOperation({ summary: "Get discount by ID" }) - @AdminRoute() - @PermissionsDec(PermissionEnum.DISCOUNTS) - @Get(":id") - findOne(@Param("id") id: string) { - return this.discountsService.findById(id); - } } diff --git a/src/modules/discounts/discounts.module.ts b/src/modules/discounts/discounts.module.ts index de11cd4..7d6b4e6 100644 --- a/src/modules/discounts/discounts.module.ts +++ b/src/modules/discounts/discounts.module.ts @@ -3,19 +3,17 @@ import { TypeOrmModule } from "@nestjs/typeorm"; import { DiscountsController } from "./discounts.controller"; import { UsersModule } from "../users/users.module"; -import { DiscountService } from "./entities/discount-product.entity"; +import { DirectDiscount } from "./entities/direct-discount.entity"; import { Discount } from "./entities/discount.entity"; import { UsageDiscount } from "./entities/usage-discount.entity"; import { DiscountsService } from "./providers/discounts.service"; -import { DiscountServicesRepository } from "./repositories/discount-services.repository"; import { DiscountRepository } from "./repositories/discounts.repository"; import { UsageDiscountRepository } from "./repositories/usage-discounts.repository"; -import { DiscountValidator } from "./utils/discount-validator"; @Module({ - imports: [TypeOrmModule.forFeature([Discount, UsageDiscount, DiscountService]), UsersModule], + imports: [TypeOrmModule.forFeature([Discount, UsageDiscount, DirectDiscount]), UsersModule], controllers: [DiscountsController], - providers: [DiscountsService, DiscountRepository, UsageDiscountRepository, DiscountServicesRepository, DiscountValidator], - exports: [DiscountsService, DiscountValidator], + providers: [DiscountsService, DiscountRepository, UsageDiscountRepository], + exports: [DiscountsService], }) export class DiscountsModule {} diff --git a/src/modules/discounts/entities/direct-discount.entity.ts b/src/modules/discounts/entities/direct-discount.entity.ts new file mode 100644 index 0000000..f72cd23 --- /dev/null +++ b/src/modules/discounts/entities/direct-discount.entity.ts @@ -0,0 +1,32 @@ +import { Column, DeleteDateColumn, Entity, OneToMany } from "typeorm"; + +import { BaseEntity } from "../../../common/entities/base.entity"; +import { DecimalTransformer } from "../../../common/transformers/decimal.transformer"; +import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity"; +import { DiscountType } from "../enums/discount-type.enum"; +@Entity() +export class DirectDiscount extends BaseEntity { + @Column({ type: "varchar", length: 150 }) + title: string; + + @Column({ type: "enum", enum: DiscountType, default: DiscountType.PERCENTAGE }) + type: DiscountType; + + @Column({ type: "decimal", precision: 16, scale: 2, transformer: new DecimalTransformer() }) + value: number; + + @Column({ type: "timestamptz" }) + startDate: Date; + + @Column({ type: "timestamptz" }) + endDate: Date; + + @Column({ type: "boolean", default: true }) + isActive: boolean; + + @DeleteDateColumn({ nullable: true }) + deletedAt?: Date; + + @OneToMany(() => SubscriptionPlan, (subscriptionPlan) => subscriptionPlan.directDiscount) + subscriptionPlans: SubscriptionPlan[]; +} diff --git a/src/modules/discounts/entities/discount-product.entity.ts b/src/modules/discounts/entities/discount-product.entity.ts deleted file mode 100644 index c2f0df4..0000000 --- a/src/modules/discounts/entities/discount-product.entity.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Entity, ManyToOne } from "typeorm"; - -import { Discount } from "./discount.entity"; -import { BaseEntity } from "../../../common/entities/base.entity"; -import { DanakService } from "../../danak-services/entities/danak-service.entity"; - -@Entity() -export class DiscountService extends BaseEntity { - @ManyToOne(() => Discount, (discount) => discount.discountServices, { nullable: false, onDelete: "CASCADE" }) - discount: Discount; - - @ManyToOne(() => DanakService, (danakService) => danakService.discountServices, { nullable: false }) - danakService: DanakService; -} diff --git a/src/modules/discounts/entities/discount.entity.ts b/src/modules/discounts/entities/discount.entity.ts index a666df9..6c082ee 100644 --- a/src/modules/discounts/entities/discount.entity.ts +++ b/src/modules/discounts/entities/discount.entity.ts @@ -1,6 +1,5 @@ -import { Column, Entity, OneToMany } from "typeorm"; +import { Column, DeleteDateColumn, Entity, OneToMany } from "typeorm"; -import { DiscountService } from "./discount-product.entity"; import { UsageDiscount } from "./usage-discount.entity"; import { BaseEntity } from "../../../common/entities/base.entity"; import { DecimalTransformer } from "../../../common/transformers/decimal.transformer"; @@ -32,18 +31,9 @@ export class Discount extends BaseEntity { @Column({ type: "boolean", default: true }) isActive: boolean; - @Column({ type: "boolean", default: false }) - direct: boolean; - - @Column({ type: "int", nullable: true }) - maxUsage: number; - - @Column({ type: "int", nullable: true }) - maxUsagePerUser: number; - - @OneToMany(() => DiscountService, (discountService) => discountService.discount, { cascade: true }) - discountServices: DiscountService[]; - @OneToMany(() => UsageDiscount, (usageDiscount) => usageDiscount.discount) usages: UsageDiscount[]; + + @DeleteDateColumn({ nullable: true }) + deletedAt?: Date; } diff --git a/src/modules/discounts/providers/discounts.service.ts b/src/modules/discounts/providers/discounts.service.ts index b431f1a..20091cd 100644 --- a/src/modules/discounts/providers/discounts.service.ts +++ b/src/modules/discounts/providers/discounts.service.ts @@ -1,46 +1,33 @@ import { randomBytes } from "node:crypto"; 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, QueryRunner } from "typeorm"; -import { DiscountMessage, InvoiceMessage } from "../../../common/enums/message.enum"; -import { Invoice } from "../../invoices/entities/invoice.entity"; -import { InvoiceStatus } from "../../invoices/enums/invoice-status.enum"; +import { DiscountMessage } from "../../../common/enums/message.enum"; import { CreateDiscountDto } from "../DTO/create-discount.dto"; -import { Discount } from "../entities/discount.entity"; -import { UsageDiscount } from "../entities/usage-discount.entity"; import { DiscountType } from "../enums/discount-type.enum"; -// import { DiscountServicesRepository } from "../repositories/discount-services.repository"; import { DiscountRepository } from "../repositories/discounts.repository"; -import { UsageDiscountRepository } from "../repositories/usage-discounts.repository"; -import { DiscountValidator } from "../utils/discount-validator"; @Injectable() export class DiscountsService { + private MAX_ATTEMPTS = 10; + constructor( private readonly discountRepository: DiscountRepository, - // private readonly discountServicesRepository: DiscountServicesRepository, - private readonly usageDiscountRepository: UsageDiscountRepository, private readonly dataSource: DataSource, - private readonly discountValidator: DiscountValidator, ) {} async create(createDiscountDto: CreateDiscountDto) { const queryRunner = this.dataSource.createQueryRunner(); - const { direct } = createDiscountDto; try { await queryRunner.connect(); await queryRunner.startTransaction(); - const discount = queryRunner.manager.create(this.discountRepository.target, createDiscountDto); - - if (!direct) { - const code = await this.generateDiscountCode(); - discount.code = code; + if (!createDiscountDto.direct) { + await this.createCodeBasedDiscount(createDiscountDto, queryRunner); + } else { + await this.createDirectBasedDiscount(createDiscountDto, queryRunner); } await queryRunner.commitTransaction(); @@ -56,271 +43,52 @@ export class DiscountsService { } } - async findById(id: string) { - const discount = await this.discountRepository.findById(id); + private async createCodeBasedDiscount(createDiscountDto: CreateDiscountDto, queryRunner: QueryRunner) { + const discount = queryRunner.manager.create(this.discountRepository.target, createDiscountDto); - if (!discount) throw new BadRequestException(DiscountMessage.NOT_FOUND); + let attempts = 0; + let code: string; + let existCode; - return { discount }; - } + do { + if (attempts >= this.MAX_ATTEMPTS) throw new BadRequestException(DiscountMessage.CODE_GENERATION_FAILED); - async validateDiscountCode(code: string, userId: string, invoice: Invoice, queryRunner: QueryRunner) { - // Find the discount by code - const discount = await queryRunner.manager.findOne(Discount, { - where: { - code, - isActive: true, - direct: false, - }, - relations: ["discountServices"], - }); - - if (!discount) { - throw new BadRequestException(DiscountMessage.INVALID_CODE); - } - - // Check if the discount is currently active (time period) - const now = dayjs(); - if (now.isBefore(dayjs(discount.startDate)) || now.isAfter(dayjs(discount.endDate))) { - throw new BadRequestException(DiscountMessage.EXPIRED); - } - - // Check if the discount has reached its maximum usage - if (discount.maxUsage !== null && discount.usedCount >= discount.maxUsage) { - throw new BadRequestException(DiscountMessage.MAX_USAGE_REACHED); - } - - // Check if the user has reached their maximum usage for this discount - if (discount.maxUsagePerUser !== null) { - const userUsageCount = await this.usageDiscountRepository.countByUserAndDiscount(userId, discount.id); - if (userUsageCount >= discount.maxUsagePerUser) { - throw new BadRequestException(DiscountMessage.MAX_USER_USAGE_REACHED); - } - } - - // Check if the discount applies to the items in the invoice - if (!this.discountValidator.validateItemsForDiscount(invoice, discount)) { - throw new BadRequestException(DiscountMessage.NOT_APPLICABLE); - } + code = await this.generateDiscountCode(); + existCode = await this.findDiscountByCode(code, queryRunner); + attempts++; + } while (existCode && attempts < this.MAX_ATTEMPTS); + discount.code = code; + discount.value = this.calculateDiscountValue(createDiscountDto); + await queryRunner.manager.save(discount); return discount; } - /** - * Applies a discount code to an invoice - * @param invoiceId The invoice ID - * @param code The discount code - * @param userId The user ID - * @returns The updated invoice - */ - async applyDiscountToInvoice(invoiceId: string, code: string, userId: string) { - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - // Get the invoice - const invoice = await queryRunner.manager.findOne(Invoice, { - where: { - id: invoiceId, - user: { id: userId }, - status: InvoiceStatus.PENDING, - }, - relations: ["items", "items.subscriptionPlan", "items.subscriptionPlan.plan", "items.subscriptionPlan.plan.service"], - }); - - if (!invoice) { - throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID); - } - - // Check if the invoice already has a discount - if (invoice.discount) { - throw new BadRequestException(InvoiceMessage.ALREADY_HAS_DISCOUNT); - } - - // Validate the discount code - const discount = await this.validateDiscountCode(code, userId, invoice, queryRunner); - - // Calculate the discount amount - const discountAmount = this.discountValidator.calculateDiscountAmount(invoice, discount); - - // Calculate the new total price - const newTotalPrice = this.discountValidator.calculateDiscountedTotalPrice(invoice, discountAmount); - - // Update the invoice - invoice.discount = discount; - invoice.totalPrice = new Decimal(newTotalPrice); - - await queryRunner.manager.save(Invoice, invoice); - - // Record the discount usage - const usageDiscount = queryRunner.manager.create(UsageDiscount, { - discount, - user: { id: userId }, - appliedAmount: discountAmount, - usedAt: new Date(), - }); - - await queryRunner.manager.save(UsageDiscount, usageDiscount); - - // Increment the discount usage count - discount.usedCount += 1; - await queryRunner.manager.save(Discount, discount); - - await queryRunner.commitTransaction(); - - return { - message: InvoiceMessage.DISCOUNT_APPLIED, - invoice, - }; - } catch (error) { - await queryRunner.rollbackTransaction(); - throw error; - } finally { - await queryRunner.release(); - } + //**************************** */ + private async createDirectBasedDiscount(createDiscountDto: CreateDiscountDto, queryRunner: QueryRunner) { + const discount = queryRunner.manager.create(this.discountRepository.target, createDiscountDto); + discount.value = this.calculateDiscountValue(createDiscountDto); + await queryRunner.manager.save(discount); + return discount; } + //**************************** */ - /** - * Cancels a discount applied to an invoice - * @param invoiceId The invoice ID - * @param userId The user ID - * @returns The updated invoice - */ - async cancelInvoiceDiscount(invoiceId: string, userId: string) { - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.connect(); - await queryRunner.startTransaction(); - - try { - // Get the invoice - const invoice = await queryRunner.manager.findOne(Invoice, { - where: { - id: invoiceId, - user: { id: userId }, - status: InvoiceStatus.PENDING, - }, - relations: ["discount", "items"], - }); - - if (!invoice) { - throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID); - } - - // Check if the invoice has a discount - if (!invoice.discount) { - throw new BadRequestException(InvoiceMessage.NO_DISCOUNT); - } - - // Find the usage record - const usageDiscount = await this.usageDiscountRepository.findByUserAndDiscount(userId, invoice.discount.id); - - if (usageDiscount) { - // Delete the usage record - await queryRunner.manager.remove(usageDiscount); - - // Decrement the discount usage count - invoice.discount.usedCount -= 1; - await queryRunner.manager.save(Discount, invoice.discount); - } - - // Recalculate the total price without the discount - // This requires recalculating based on the original item prices - const originalTotalPrice = invoice.items.reduce((sum, item) => { - const itemTotal = new Decimal(item.unitPrice) - .mul(item.count) - .minus(new Decimal(item.unitPrice).mul(item.count).mul(item.discount).div(100)); - return sum.add(itemTotal); - }, new Decimal(0)); - - // Add tax to the total - const totalWithTax = originalTotalPrice.add(invoice.tax || 0); - - // Remove the discount from the invoice - // invoice.discount = null; - invoice.totalPrice = new Decimal(totalWithTax); - - await queryRunner.manager.save(Invoice, invoice); - - await queryRunner.commitTransaction(); - - return { - message: InvoiceMessage.DISCOUNT_CANCELED, - invoice, - }; - } catch (error) { - await queryRunner.rollbackTransaction(); - throw error; - } finally { - await queryRunner.release(); - } + private async findDiscountByCode(code: string, queryRunner: QueryRunner) { + const discount = await queryRunner.manager.findOne(this.discountRepository.target, { where: { code } }); + return discount; } + //**************************** */ - /** - * Applies a direct discount to a service price - * When calculating service prices, this discount should be considered - * @param serviceId The service ID - * @returns The discount information for the service - */ - async getDirectDiscountForService(serviceId: string) { - // Find active direct discounts for this service - const discounts = await this.discountRepository - .createQueryBuilder("discount") - .innerJoin("discount.discountServices", "ds") - .where("discount.isActive = :isActive", { isActive: true }) - .andWhere("discount.direct = :direct", { direct: true }) - .andWhere("discount.startDate <= :now", { now: new Date() }) - .andWhere("discount.endDate >= :now", { now: new Date() }) - .andWhere("ds.danakService.id = :serviceId", { serviceId }) - .getMany(); - - if (!discounts || discounts.length === 0) { - return null; - } - - // For simplicity, we're returning the first valid discount - // In a more complex system, you might want to apply the best discount - return discounts[0]; - } - - /** - * Calculate the discounted price for a service - * @param serviceId The service ID - * @param originalPrice The original price - * @returns The discounted price and discount info - */ - async calculateDiscountedPrice(serviceId: string, originalPrice: number) { - const discount = await this.getDirectDiscountForService(serviceId); - - if (!discount) { - return { - originalPrice, - discountedPrice: originalPrice, - discount: null, - }; - } - - let discountedPrice: number; - - if (discount.type === DiscountType.PERCENTAGE) { - discountedPrice = new Decimal(originalPrice) - .mul(100 - discount.value) - .div(100) - .toNumber(); - } else { - discountedPrice = Math.max(0, originalPrice - discount.value); - } - - return { - originalPrice, - discountedPrice, - discount, - }; - } - - //*************************** */ - private async generateDiscountCode() { - const code = randomBytes(8).toString("hex"); + private async generateDiscountCode(length: number = 8) { + const code = randomBytes(length).toString("hex"); return code; } + //**************************** */ + + private calculateDiscountValue(createDiscountDto: CreateDiscountDto): number { + if (createDiscountDto.type === DiscountType.PERCENTAGE) return createDiscountDto.value; + if (createDiscountDto.type === DiscountType.FIXED_AMOUNT) return createDiscountDto.value; + + throw new BadRequestException(DiscountMessage.INVALID_DISCOUNT_TYPE); + } } diff --git a/src/modules/discounts/repositories/discount-services.repository.ts b/src/modules/discounts/repositories/discount-services.repository.ts deleted file mode 100644 index 6980b89..0000000 --- a/src/modules/discounts/repositories/discount-services.repository.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { InjectRepository } from "@nestjs/typeorm"; -import { Repository } from "typeorm"; - -import { DiscountService } from "../entities/discount-product.entity"; -@Injectable() -export class DiscountServicesRepository extends Repository { - constructor(@InjectRepository(DiscountService) repository: Repository) { - super(repository.target, repository.manager, repository.queryRunner); - } - - async createMany(discountId: string, productIds: string[]): Promise { - const discountServices = productIds.map((productId) => - this.create({ - discount: { id: discountId }, - danakService: { id: productId }, - }), - ); - return this.save(discountServices); - } - - async findByDiscountId(discountId: string): Promise { - return this.find({ - where: { discount: { id: discountId } }, - }); - } - - async findByProductId(productId: string): Promise { - return this.find({ - where: { danakService: { id: productId } }, - relations: ["discount"], - }); - } - - async deleteByDiscountId(discountId: string): Promise { - await this.createQueryBuilder().delete().where("discountId = :discountId", { discountId }).execute(); - } - - async hasProductForDiscount(discountId: string, productId: string): Promise { - const count = await this.count({ - where: { - discount: { id: discountId }, - danakService: { id: productId }, - }, - }); - return count > 0; - } - - async findDiscountIdsForProduct(productId: string): Promise { - const discountServices = await this.find({ - where: { danakService: { id: productId } }, - select: ["discount"], - relations: ["discount"], - }); - return discountServices.map((dp) => dp.discount.id); - } -} diff --git a/src/modules/discounts/utils/discount-validator.ts b/src/modules/discounts/utils/discount-validator.ts deleted file mode 100644 index f3bd2b2..0000000 --- a/src/modules/discounts/utils/discount-validator.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { Injectable } from "@nestjs/common"; -// eslint-disable-next-line import/no-named-as-default -import Decimal from "decimal.js"; - -import { InvoiceItem } from "../../invoices/entities/invoice-item.entity"; -import { Invoice } from "../../invoices/entities/invoice.entity"; -import { Discount } from "../entities/discount.entity"; -import { DiscountType } from "../enums/discount-type.enum"; - -@Injectable() -export class DiscountValidator { - /** - * Validates if the items in an invoice match with the services targeted by the discount - * @param invoice The invoice to check - * @param discount The discount to apply - * @returns true if valid, false otherwise - */ - public validateItemsForDiscount(invoice: Invoice, discount: Discount): boolean { - // If discount has no target services, it applies to all - if (!discount.discountServices || discount.discountServices.length === 0) { - return true; - } - - // Check if any item in the invoice matches the targeted services - const targetServiceIds = discount.discountServices.map((ds) => ds.danakService.id); - - // This implementation assumes there's a way to map from invoice items to services - // You'll need to adapt this based on your data model - const hasMatchingItem = invoice.items.some((item) => { - // For subscription-based items - if (item.subscriptionPlan && item.subscriptionPlan.plan && item.subscriptionPlan.plan.service) { - return targetServiceIds.includes(item.subscriptionPlan.plan.service.id); - } - - // For other types of items - implement according to your data model - // This is a placeholder - you need to adapt this to your specific implementation - return false; - }); - - return hasMatchingItem; - } - - /** - * Calculates the discount amount to apply to the invoice - * @param invoice The invoice to apply the discount to - * @param discount The discount to apply - * @returns The discount amount - */ - public calculateDiscountAmount(invoice: Invoice, discount: Discount): Decimal { - if (discount.type === DiscountType.PERCENTAGE) { - return new Decimal(invoice.totalPrice).mul(discount.value).div(100); - } else { - // For fixed amount, cap at the invoice total - return Decimal.min(new Decimal(discount.value), new Decimal(invoice.totalPrice)); - } - } - - /** - * Calculates the discounted price for an item - * @param item The item to calculate discount for - * @param discount The discount to apply - * @returns The discounted price - */ - public calculateItemDiscountedPrice(item: InvoiceItem, discount: Discount): Decimal { - const originalPrice = new Decimal(item.unitPrice).mul(item.count); - - if (discount.type === DiscountType.PERCENTAGE) { - return originalPrice.mul(new Decimal(100).minus(discount.value)).div(100); - } else { - // For fixed amount discounts, proportionally distribute the discount among items - // This is a simple implementation - you might want a more complex distribution logic - const discountAmount = Decimal.min(new Decimal(discount.value), originalPrice); - return originalPrice.minus(discountAmount); - } - } - - /** - * Calculates the updated total price after applying a discount - * @param invoice The invoice to calculate for - * @param discountAmount The discount amount to apply - * @returns The new total price as a number - */ - public calculateDiscountedTotalPrice(invoice: Invoice, discountAmount: Decimal): number { - return new Decimal(invoice.totalPrice).minus(discountAmount).toNumber(); - } -} diff --git a/src/modules/invoices/invoices.controller.ts b/src/modules/invoices/invoices.controller.ts index 8915f81..efd053d 100755 --- a/src/modules/invoices/invoices.controller.ts +++ b/src/modules/invoices/invoices.controller.ts @@ -1,7 +1,6 @@ import { Body, Controller, Get, Param, Patch, Post, Query } from "@nestjs/common"; import { ApiOperation } from "@nestjs/swagger"; -import { ApplyDiscountDto } from "./DTO/apply-discount.dto"; import { CreateInvoiceDto } from "./DTO/create-invoice.dto"; import { InvoicesSearchQueryDto, UserInvoicesSearchQueryDto } from "./DTO/invoices-search-query.dto"; import { UpdateInvoiceDto } from "./DTO/update-invoice.dto"; @@ -12,16 +11,12 @@ import { PermissionsDec } from "../../common/decorators/permission.decorator"; import { UserDec } from "../../common/decorators/user.decorator"; import { ParamDto } from "../../common/DTO/param.dto"; import { VerifyOtpWithUserId } from "../auth/DTO/verify-otp.dto"; -import { DiscountsService } from "../discounts/providers/discounts.service"; import { PermissionEnum } from "../users/enums/permission.enum"; @Controller("invoices") @AuthGuards() export class InvoicesController { - constructor( - private readonly invoiceService: InvoicesService, - private readonly discountsService: DiscountsService, - ) {} + constructor(private readonly invoiceService: InvoicesService) {} @ApiOperation({ summary: "create an invoice ==> admin route" }) @PermissionsDec(PermissionEnum.INVOICES) @@ -76,15 +71,15 @@ export class InvoicesController { return this.invoiceService.payInvoice(paramDto.id, userId); } - @ApiOperation({ summary: "apply discount on invoice by user" }) - @Post(":id/apply-discount") - async applyDiscount(@Param() paramDto: ParamDto, @Body() applyDiscountDto: ApplyDiscountDto, @UserDec("id") userId: string) { - return this.discountsService.applyDiscountToInvoice(paramDto.id, applyDiscountDto.code, userId); - } + // @ApiOperation({ summary: "apply discount on invoice by user" }) + // @Post(":id/apply-discount") + // async applyDiscount(@Param() paramDto: ParamDto, @Body() applyDiscountDto: ApplyDiscountDto, @UserDec("id") userId: string) { + // return this.discountsService.applyDiscountToInvoice(paramDto.id, applyDiscountDto.code, userId); + // } - @ApiOperation({ summary: "cancel discount by user" }) - @Post(":id/cancel-discount") - cancelDiscount(@Param() paramDto: ParamDto, @UserDec("id") userId: string) { - return this.discountsService.cancelInvoiceDiscount(paramDto.id, userId); - } + // @ApiOperation({ summary: "cancel discount by user" }) + // @Post(":id/cancel-discount") + // cancelDiscount(@Param() paramDto: ParamDto, @UserDec("id") userId: string) { + // return this.discountsService.cancelInvoiceDiscount(paramDto.id, userId); + // } } diff --git a/src/modules/referrals/providers/referrals.service.ts b/src/modules/referrals/providers/referrals.service.ts index 3f3f8c2..fb52793 100644 --- a/src/modules/referrals/providers/referrals.service.ts +++ b/src/modules/referrals/providers/referrals.service.ts @@ -14,6 +14,7 @@ import { ReferralRewardsRepository } from "../repositories/referral-rewards.repo import { ReferralsRepository } from "../repositories/referrals.repository"; @Injectable() export class ReferralsService { + private MAX_ATTEMPTS = 10; constructor( private readonly referralsRepository: ReferralsRepository, private readonly referralCodesRepository: ReferralCodesRepository, @@ -25,13 +26,12 @@ export class ReferralsService { if (existReferralCode) throw new BadRequestException(ReferralMessage.CODE_EXIST_FOR_USER); - const MAX_ATTEMPTS = 10; let attempts = 0; let code: string; let existCode; do { - if (attempts >= MAX_ATTEMPTS) { + if (attempts >= this.MAX_ATTEMPTS) { throw new BadRequestException(ReferralMessage.CODE_GENERATION_FAILED); } diff --git a/src/modules/subscriptions/entities/subscription.entity.ts b/src/modules/subscriptions/entities/subscription.entity.ts index ba2fd87..de71bf6 100755 --- a/src/modules/subscriptions/entities/subscription.entity.ts +++ b/src/modules/subscriptions/entities/subscription.entity.ts @@ -6,7 +6,7 @@ import { UserSubscription } from "./user-subscription.entity"; 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 { DirectDiscount } from "../../discounts/entities/direct-discount.entity"; @Entity() @Index(["service", "name"], { unique: true }) @@ -19,7 +19,7 @@ export class SubscriptionPlan extends BaseEntity { @Column({ type: "decimal", precision: 16, scale: 2, nullable: false, transformer: new DecimalTransformer() }) price: Decimal; - // + @Column({ type: "boolean", default: true }) isActive: boolean; @@ -29,6 +29,6 @@ export class SubscriptionPlan extends BaseEntity { @OneToMany(() => UserSubscription, (userSubscription) => userSubscription.plan) userSubscriptions: UserSubscription[]; - // @ManyToMany(() => Discount, (discount) => discount.subscriptionPlans) - // discounts: Discount[]; + @ManyToOne(() => DirectDiscount, (directDiscount) => directDiscount.subscriptionPlans, { nullable: true }) + directDiscount?: DirectDiscount; }