chore: complete the discount service and test the logic to create
This commit is contained in:
@@ -114,6 +114,7 @@ export const enum CommonMessage {
|
|||||||
ID_REQUIRED = "شناسه مورد نیاز است",
|
ID_REQUIRED = "شناسه مورد نیاز است",
|
||||||
ID_SHOULD_BE_UUID = "شناسه باید یک یو یو آی دی باشد",
|
ID_SHOULD_BE_UUID = "شناسه باید یک یو یو آی دی باشد",
|
||||||
PaymentTypeQueryNotEmpty = "نوع پرداخت نمیتواند خالی باشد",
|
PaymentTypeQueryNotEmpty = "نوع پرداخت نمیتواند خالی باشد",
|
||||||
|
SEARCH_QUERY_STRING = "رشته جستجو باید یک رشته باشد",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum CategoryMessage {
|
export const enum CategoryMessage {
|
||||||
@@ -521,6 +522,8 @@ export const enum DiscountMessage {
|
|||||||
START_DATE_MUST_BE_BEFORE_END_DATE = "تاریخ شروع باید قبل از تاریخ پایان باشد",
|
START_DATE_MUST_BE_BEFORE_END_DATE = "تاریخ شروع باید قبل از تاریخ پایان باشد",
|
||||||
START_DATE_MUST_BE_AFTER_NOW = "تاریخ شروع باید بعد از تاریخ فعلی باشد",
|
START_DATE_MUST_BE_AFTER_NOW = "تاریخ شروع باید بعد از تاریخ فعلی باشد",
|
||||||
END_DATE_MUST_BE_AFTER_NOW = "تاریخ پایان باید بعد از تاریخ فعلی باشد",
|
END_DATE_MUST_BE_AFTER_NOW = "تاریخ پایان باید بعد از تاریخ فعلی باشد",
|
||||||
|
EACH_TARGET_PRODUCT_MUST_BE_A_UUID = "هر محصول باید یک UUID معتبر باشد",
|
||||||
|
DIRECT_DISCOUNT_ALREADY_EXISTS = "تخفیف مستقیم قبلا برای سرویس [serviceName] ثبت شده است",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum EmailMessage {
|
export const enum EmailMessage {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
import { IsArray, IsBoolean, IsDateString, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateIf } from "class-validator";
|
import { IsArray, IsBoolean, IsDateString, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, IsUUID, ValidateIf } from "class-validator";
|
||||||
|
|
||||||
import { DiscountMessage } from "../../../common/enums/message.enum";
|
import { DiscountMessage } from "../../../common/enums/message.enum";
|
||||||
import { DiscountType } from "../enums/discount-type.enum";
|
import { DiscountType } from "../enums/discount-type.enum";
|
||||||
@@ -51,7 +51,7 @@ export class CreateDiscountDto {
|
|||||||
@IsNotEmpty({ message: DiscountMessage.TARGET_PRODUCTS_MUST_BE_AN_ARRAY })
|
@IsNotEmpty({ message: DiscountMessage.TARGET_PRODUCTS_MUST_BE_AN_ARRAY })
|
||||||
@IsArray({ message: DiscountMessage.TARGET_PRODUCTS_MUST_BE_AN_ARRAY })
|
@IsArray({ message: DiscountMessage.TARGET_PRODUCTS_MUST_BE_AN_ARRAY })
|
||||||
@ValidateIf((o) => o.direct)
|
@ValidateIf((o) => o.direct)
|
||||||
@IsString({ each: true, message: DiscountMessage.EACH_TARGET_PRODUCT_MUST_BE_A_STRING })
|
@IsUUID("4", { each: true, message: DiscountMessage.EACH_TARGET_PRODUCT_MUST_BE_A_UUID })
|
||||||
@ApiPropertyOptional({
|
@ApiPropertyOptional({
|
||||||
description: "Array of service IDs this discount applies to (empty array means applies to all services)",
|
description: "Array of service IDs this discount applies to (empty array means applies to all services)",
|
||||||
example: ["service-id-1", "service-id-2"],
|
example: ["service-id-1", "service-id-2"],
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { IsOptional, IsString } from "class-validator";
|
||||||
|
|
||||||
|
import { PaginationDto } from "../../../common/DTO/pagination.dto";
|
||||||
|
import { CommonMessage } from "../../../common/enums/message.enum";
|
||||||
|
|
||||||
|
export class SearchDiscountQueryDto extends PaginationDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString({ message: CommonMessage.SEARCH_QUERY_STRING })
|
||||||
|
@ApiPropertyOptional({ description: "Search query", example: "search query" })
|
||||||
|
q?: string;
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Body, Controller, Post } from "@nestjs/common";
|
import { Body, Controller, Get, Post, Query } from "@nestjs/common";
|
||||||
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { CreateDiscountDto } from "./DTO/create-discount.dto";
|
import { CreateDiscountDto } from "./DTO/create-discount.dto";
|
||||||
|
import { SearchDiscountQueryDto } from "./DTO/search-discount-query.dto";
|
||||||
import { DiscountsService } from "./providers/discounts.service";
|
import { DiscountsService } from "./providers/discounts.service";
|
||||||
import { AdminRoute } from "../../common/decorators/admin.decorator";
|
import { AdminRoute } from "../../common/decorators/admin.decorator";
|
||||||
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||||
@@ -22,4 +23,13 @@ export class DiscountsController {
|
|||||||
create(@Body() createDiscountDto: CreateDiscountDto) {
|
create(@Body() createDiscountDto: CreateDiscountDto) {
|
||||||
return this.discountsService.create(createDiscountDto);
|
return this.discountsService.create(createDiscountDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "Get all discounts for admin" })
|
||||||
|
@AdminRoute()
|
||||||
|
@PermissionsDec(PermissionEnum.DISCOUNTS)
|
||||||
|
@Get()
|
||||||
|
@ApiResponse({ status: 200, description: "Discounts fetched successfully" })
|
||||||
|
getDiscountsForAdmin(@Query() queryDto: SearchDiscountQueryDto) {
|
||||||
|
return this.discountsService.getDiscountsForAdmin(queryDto);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,16 @@ import { TypeOrmModule } from "@nestjs/typeorm";
|
|||||||
|
|
||||||
import { DiscountsController } from "./discounts.controller";
|
import { DiscountsController } from "./discounts.controller";
|
||||||
import { UsersModule } from "../users/users.module";
|
import { UsersModule } from "../users/users.module";
|
||||||
import { DirectDiscount } from "./entities/direct-discount.entity";
|
|
||||||
import { Discount } from "./entities/discount.entity";
|
import { Discount } from "./entities/discount.entity";
|
||||||
import { UsageDiscount } from "./entities/usage-discount.entity";
|
import { UsageDiscount } from "./entities/usage-discount.entity";
|
||||||
import { DiscountsService } from "./providers/discounts.service";
|
import { DiscountsService } from "./providers/discounts.service";
|
||||||
import { DirectDiscountRepository } from "./repositories/direct-discounts.repository";
|
|
||||||
import { DiscountRepository } from "./repositories/discounts.repository";
|
import { DiscountRepository } from "./repositories/discounts.repository";
|
||||||
import { UsageDiscountRepository } from "./repositories/usage-discounts.repository";
|
import { UsageDiscountRepository } from "./repositories/usage-discounts.repository";
|
||||||
import { SubscriptionsModule } from "../subscriptions/subscriptions.module";
|
import { SubscriptionsModule } from "../subscriptions/subscriptions.module";
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Discount, UsageDiscount, DirectDiscount]), UsersModule, SubscriptionsModule],
|
imports: [TypeOrmModule.forFeature([Discount, UsageDiscount]), UsersModule, SubscriptionsModule],
|
||||||
controllers: [DiscountsController],
|
controllers: [DiscountsController],
|
||||||
providers: [DiscountsService, DiscountRepository, DirectDiscountRepository, UsageDiscountRepository],
|
providers: [DiscountsService, DiscountRepository, UsageDiscountRepository],
|
||||||
exports: [DiscountsService],
|
exports: [DiscountsService],
|
||||||
})
|
})
|
||||||
export class DiscountsModule {}
|
export class DiscountsModule {}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
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[];
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,8 @@ import { Column, DeleteDateColumn, Entity, OneToMany } from "typeorm";
|
|||||||
import { UsageDiscount } from "./usage-discount.entity";
|
import { UsageDiscount } from "./usage-discount.entity";
|
||||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||||
import { DecimalTransformer } from "../../../common/transformers/decimal.transformer";
|
import { DecimalTransformer } from "../../../common/transformers/decimal.transformer";
|
||||||
|
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
|
||||||
|
import { DiscountApplicationType } from "../enums/discount-application-type.enum";
|
||||||
import { DiscountType } from "../enums/discount-type.enum";
|
import { DiscountType } from "../enums/discount-type.enum";
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@@ -10,8 +12,11 @@ export class Discount extends BaseEntity {
|
|||||||
@Column({ type: "varchar", length: 150 })
|
@Column({ type: "varchar", length: 150 })
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
@Column({ type: "varchar", length: 100, unique: true })
|
@Column({ type: "enum", enum: DiscountApplicationType })
|
||||||
code: string;
|
applicationType: DiscountApplicationType;
|
||||||
|
|
||||||
|
@Column({ type: "varchar", length: 100, unique: true, nullable: true })
|
||||||
|
code?: string;
|
||||||
|
|
||||||
@Column({ type: "enum", enum: DiscountType, default: DiscountType.PERCENTAGE })
|
@Column({ type: "enum", enum: DiscountType, default: DiscountType.PERCENTAGE })
|
||||||
type: DiscountType;
|
type: DiscountType;
|
||||||
@@ -31,9 +36,13 @@ export class Discount extends BaseEntity {
|
|||||||
@Column({ type: "boolean", default: true })
|
@Column({ type: "boolean", default: true })
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
|
|
||||||
|
@DeleteDateColumn({ nullable: true })
|
||||||
|
deletedAt?: Date;
|
||||||
|
|
||||||
|
// Relations
|
||||||
@OneToMany(() => UsageDiscount, (usageDiscount) => usageDiscount.discount)
|
@OneToMany(() => UsageDiscount, (usageDiscount) => usageDiscount.discount)
|
||||||
usages: UsageDiscount[];
|
usages: UsageDiscount[];
|
||||||
|
|
||||||
@DeleteDateColumn({ nullable: true })
|
@OneToMany(() => SubscriptionPlan, (subscriptionPlan) => subscriptionPlan.directDiscount)
|
||||||
deletedAt?: Date;
|
subscriptionPlans: SubscriptionPlan[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export enum DiscountApplicationType {
|
||||||
|
CODE_BASED = "CODE_BASED",
|
||||||
|
DIRECT = "DIRECT",
|
||||||
|
}
|
||||||
@@ -7,22 +7,22 @@ import { DataSource, QueryRunner } from "typeorm";
|
|||||||
import { DiscountMessage } from "../../../common/enums/message.enum";
|
import { DiscountMessage } from "../../../common/enums/message.enum";
|
||||||
import { SubscriptionsService } from "../../subscriptions/providers/subscriptions.service";
|
import { SubscriptionsService } from "../../subscriptions/providers/subscriptions.service";
|
||||||
import { CreateDiscountDto } from "../DTO/create-discount.dto";
|
import { CreateDiscountDto } from "../DTO/create-discount.dto";
|
||||||
|
import { SearchDiscountQueryDto } from "../DTO/search-discount-query.dto";
|
||||||
|
import { Discount } from "../entities/discount.entity";
|
||||||
|
import { DiscountApplicationType } from "../enums/discount-application-type.enum";
|
||||||
import { DiscountType } from "../enums/discount-type.enum";
|
import { DiscountType } from "../enums/discount-type.enum";
|
||||||
import { DirectDiscountRepository } from "../repositories/direct-discounts.repository";
|
|
||||||
import { DiscountRepository } from "../repositories/discounts.repository";
|
import { DiscountRepository } from "../repositories/discounts.repository";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DiscountsService {
|
export class DiscountsService {
|
||||||
private MAX_ATTEMPTS = 10;
|
private MAX_ATTEMPTS = 10;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly discountRepository: DiscountRepository,
|
private readonly discountRepository: DiscountRepository,
|
||||||
private readonly directDiscountRepository: DirectDiscountRepository,
|
|
||||||
private readonly subscriptionService: SubscriptionsService,
|
private readonly subscriptionService: SubscriptionsService,
|
||||||
private readonly dataSource: DataSource,
|
private readonly dataSource: DataSource,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
//***************************************** */
|
|
||||||
|
|
||||||
async create(createDiscountDto: CreateDiscountDto) {
|
async create(createDiscountDto: CreateDiscountDto) {
|
||||||
const queryRunner = this.dataSource.createQueryRunner();
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
|
|
||||||
@@ -32,16 +32,13 @@ export class DiscountsService {
|
|||||||
|
|
||||||
await this.validateDates(createDiscountDto.startDate, createDiscountDto.endDate);
|
await this.validateDates(createDiscountDto.startDate, createDiscountDto.endDate);
|
||||||
|
|
||||||
if (!createDiscountDto.direct) {
|
const discount = await this.createDiscount(createDiscountDto, queryRunner);
|
||||||
await this.createCodeBasedDiscount(createDiscountDto, queryRunner);
|
|
||||||
} else {
|
|
||||||
await this.createDirectBasedDiscount(createDiscountDto, queryRunner);
|
|
||||||
}
|
|
||||||
|
|
||||||
await queryRunner.commitTransaction();
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: DiscountMessage.CREATED,
|
message: DiscountMessage.CREATED,
|
||||||
|
discount,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await queryRunner.rollbackTransaction();
|
await queryRunner.rollbackTransaction();
|
||||||
@@ -50,10 +47,125 @@ export class DiscountsService {
|
|||||||
await queryRunner.release();
|
await queryRunner.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//***************************************** */
|
|
||||||
private async createCodeBasedDiscount(createDiscountDto: CreateDiscountDto, queryRunner: QueryRunner) {
|
|
||||||
const discount = queryRunner.manager.create(this.discountRepository.target, createDiscountDto);
|
|
||||||
|
|
||||||
|
async getDiscountsForAdmin(queryDto: SearchDiscountQueryDto) {
|
||||||
|
const [discounts, count] = await this.discountRepository.findDiscountForAdmin(queryDto);
|
||||||
|
|
||||||
|
return {
|
||||||
|
discounts,
|
||||||
|
count,
|
||||||
|
pagination: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateDiscount(id: string, updateDiscountDto: CreateDiscountDto) {
|
||||||
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await queryRunner.connect();
|
||||||
|
await queryRunner.startTransaction();
|
||||||
|
|
||||||
|
const existingDiscount = await this.discountRepository.findOne({
|
||||||
|
where: { id },
|
||||||
|
relations: ["subscriptionPlans"],
|
||||||
|
withDeleted: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingDiscount) {
|
||||||
|
throw new BadRequestException(DiscountMessage.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.validateDates(updateDiscountDto.startDate, updateDiscountDto.endDate);
|
||||||
|
|
||||||
|
// Update basic discount properties
|
||||||
|
Object.assign(existingDiscount, {
|
||||||
|
...updateDiscountDto,
|
||||||
|
applicationType: updateDiscountDto.direct ? DiscountApplicationType.DIRECT : DiscountApplicationType.CODE_BASED,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle code-based discount updates
|
||||||
|
if (!updateDiscountDto.direct) {
|
||||||
|
if (updateDiscountDto.code && updateDiscountDto.code !== existingDiscount.code) {
|
||||||
|
const existingCode = await this.findDiscountByCode(updateDiscountDto.code, queryRunner);
|
||||||
|
if (existingCode) {
|
||||||
|
throw new BadRequestException(DiscountMessage.CODE_ALREADY_EXISTS);
|
||||||
|
}
|
||||||
|
existingDiscount.code = updateDiscountDto.code;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Clear code for direct discounts
|
||||||
|
existingDiscount.code = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate and update value
|
||||||
|
existingDiscount.value = this.calculateDiscountValue(updateDiscountDto);
|
||||||
|
|
||||||
|
// Save the updated discount
|
||||||
|
await queryRunner.manager.save(existingDiscount);
|
||||||
|
|
||||||
|
// Handle direct discount updates
|
||||||
|
if (updateDiscountDto.direct) {
|
||||||
|
await this.updateDirectDiscount(updateDiscountDto, existingDiscount, queryRunner);
|
||||||
|
}
|
||||||
|
|
||||||
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: DiscountMessage.UPDATED,
|
||||||
|
discount: existingDiscount,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await queryRunner.rollbackTransaction();
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await queryRunner.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async softDeleteDiscount(id: string): Promise<void> {
|
||||||
|
await this.discountRepository.softDeleteDiscount(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createDiscount(createDiscountDto: CreateDiscountDto, queryRunner: QueryRunner) {
|
||||||
|
const discount = queryRunner.manager.create(this.discountRepository.target, {
|
||||||
|
...createDiscountDto,
|
||||||
|
applicationType: createDiscountDto.direct ? DiscountApplicationType.DIRECT : DiscountApplicationType.CODE_BASED,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!createDiscountDto.direct) {
|
||||||
|
await this.generateDiscountCode(discount, queryRunner);
|
||||||
|
}
|
||||||
|
|
||||||
|
discount.value = this.calculateDiscountValue(createDiscountDto);
|
||||||
|
await queryRunner.manager.save(discount);
|
||||||
|
|
||||||
|
if (createDiscountDto.direct) {
|
||||||
|
await this.applyDirectDiscount(createDiscountDto, discount, queryRunner);
|
||||||
|
}
|
||||||
|
|
||||||
|
return discount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async applyDirectDiscount(createDiscountDto: CreateDiscountDto, discount: Discount, queryRunner: QueryRunner) {
|
||||||
|
const subscriptionPlans = await this.subscriptionService.getAllServiceSubscriptions(
|
||||||
|
createDiscountDto.targetServices ?? [],
|
||||||
|
queryRunner,
|
||||||
|
);
|
||||||
|
|
||||||
|
const plansWithExistingDiscount = subscriptionPlans.filter((plan) => plan.directDiscount);
|
||||||
|
if (plansWithExistingDiscount.length > 0) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
DiscountMessage.DIRECT_DISCOUNT_ALREADY_EXISTS.replace("[serviceName]", plansWithExistingDiscount[0].service.name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const subscriptionPlan of subscriptionPlans) {
|
||||||
|
subscriptionPlan.directDiscount = discount;
|
||||||
|
await queryRunner.manager.save(subscriptionPlan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generateDiscountCode(discount: Discount, queryRunner: QueryRunner) {
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
let code: string;
|
let code: string;
|
||||||
let existCode;
|
let existCode;
|
||||||
@@ -61,46 +173,24 @@ export class DiscountsService {
|
|||||||
do {
|
do {
|
||||||
if (attempts >= this.MAX_ATTEMPTS) throw new BadRequestException(DiscountMessage.CODE_GENERATION_FAILED);
|
if (attempts >= this.MAX_ATTEMPTS) throw new BadRequestException(DiscountMessage.CODE_GENERATION_FAILED);
|
||||||
|
|
||||||
code = await this.generateDiscountCode();
|
code = await this.generateRandomCode();
|
||||||
existCode = await this.findDiscountByCode(code, queryRunner);
|
existCode = await this.findDiscountByCode(code, queryRunner);
|
||||||
attempts++;
|
attempts++;
|
||||||
} while (existCode && attempts < this.MAX_ATTEMPTS);
|
} while (existCode && attempts < this.MAX_ATTEMPTS);
|
||||||
|
|
||||||
discount.code = code;
|
discount.code = code;
|
||||||
discount.value = this.calculateDiscountValue(createDiscountDto);
|
|
||||||
await queryRunner.manager.save(discount);
|
|
||||||
return discount;
|
|
||||||
}
|
}
|
||||||
//***************************************** */
|
|
||||||
private async createDirectBasedDiscount(createDiscountDto: CreateDiscountDto, queryRunner: QueryRunner) {
|
|
||||||
// Create the discount entity
|
|
||||||
const discount = queryRunner.manager.create(this.directDiscountRepository.target, createDiscountDto);
|
|
||||||
discount.value = this.calculateDiscountValue(createDiscountDto);
|
|
||||||
|
|
||||||
await queryRunner.manager.save(discount);
|
|
||||||
|
|
||||||
const subscriptionPlans = await this.subscriptionService.getAllServiceSubscriptions(createDiscountDto.targetServices ?? []);
|
|
||||||
|
|
||||||
for (const subscriptionPlan of subscriptionPlans) {
|
|
||||||
subscriptionPlan.directDiscount = discount;
|
|
||||||
await queryRunner.manager.save(subscriptionPlan);
|
|
||||||
}
|
|
||||||
|
|
||||||
return discount;
|
|
||||||
}
|
|
||||||
//**************************** */
|
|
||||||
|
|
||||||
private async findDiscountByCode(code: string, queryRunner: QueryRunner) {
|
private async findDiscountByCode(code: string, queryRunner: QueryRunner) {
|
||||||
const discount = await queryRunner.manager.findOne(this.discountRepository.target, { where: { code } });
|
return queryRunner.manager.findOne(this.discountRepository.target, {
|
||||||
return discount;
|
where: { code },
|
||||||
|
withDeleted: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
//**************************** */
|
|
||||||
|
|
||||||
private async generateDiscountCode(length: number = 8) {
|
private async generateRandomCode(length: number = 4) {
|
||||||
const code = randomBytes(length).toString("hex");
|
return randomBytes(length).toString("hex").toUpperCase();
|
||||||
return code;
|
|
||||||
}
|
}
|
||||||
//**************************** */
|
|
||||||
|
|
||||||
private calculateDiscountValue(createDiscountDto: CreateDiscountDto): number {
|
private calculateDiscountValue(createDiscountDto: CreateDiscountDto): number {
|
||||||
if (createDiscountDto.type === DiscountType.PERCENTAGE) return createDiscountDto.value;
|
if (createDiscountDto.type === DiscountType.PERCENTAGE) return createDiscountDto.value;
|
||||||
@@ -109,8 +199,6 @@ export class DiscountsService {
|
|||||||
throw new BadRequestException(DiscountMessage.INVALID_DISCOUNT_TYPE);
|
throw new BadRequestException(DiscountMessage.INVALID_DISCOUNT_TYPE);
|
||||||
}
|
}
|
||||||
|
|
||||||
//**************************** */
|
|
||||||
|
|
||||||
private async validateDates(startDate: string, endDate: string) {
|
private async validateDates(startDate: string, endDate: string) {
|
||||||
if (dayjs(startDate).isAfter(dayjs(endDate))) {
|
if (dayjs(startDate).isAfter(dayjs(endDate))) {
|
||||||
throw new BadRequestException(DiscountMessage.START_DATE_MUST_BE_BEFORE_END_DATE);
|
throw new BadRequestException(DiscountMessage.START_DATE_MUST_BE_BEFORE_END_DATE);
|
||||||
@@ -128,4 +216,39 @@ export class DiscountsService {
|
|||||||
throw new BadRequestException(DiscountMessage.END_DATE_MUST_BE_AFTER_NOW);
|
throw new BadRequestException(DiscountMessage.END_DATE_MUST_BE_AFTER_NOW);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async updateDirectDiscount(updateDiscountDto: CreateDiscountDto, discount: Discount, queryRunner: QueryRunner) {
|
||||||
|
const subscriptionPlans = await this.subscriptionService.getAllServiceSubscriptions(
|
||||||
|
updateDiscountDto.targetServices ?? [],
|
||||||
|
queryRunner,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Remove discount from plans that are no longer in the target services
|
||||||
|
const currentPlans = await this.subscriptionService.getAllServiceSubscriptions(
|
||||||
|
discount.subscriptionPlans.map((plan) => plan.service.id),
|
||||||
|
queryRunner,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const plan of currentPlans) {
|
||||||
|
if (!subscriptionPlans.some((newPlan) => newPlan.id === plan.id)) {
|
||||||
|
plan.directDiscount = null;
|
||||||
|
await queryRunner.manager.save(plan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for existing discounts in new target plans
|
||||||
|
const plansWithExistingDiscount = subscriptionPlans.filter((plan) => plan.directDiscount && plan.directDiscount.id !== discount.id);
|
||||||
|
|
||||||
|
if (plansWithExistingDiscount.length > 0) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
DiscountMessage.DIRECT_DISCOUNT_ALREADY_EXISTS.replace("[serviceName]", plansWithExistingDiscount[0].service.name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply discount to new target plans
|
||||||
|
for (const subscriptionPlan of subscriptionPlans) {
|
||||||
|
subscriptionPlan.directDiscount = discount;
|
||||||
|
await queryRunner.manager.save(subscriptionPlan);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
|
||||||
import { InjectRepository } from "@nestjs/typeorm";
|
|
||||||
import { Repository } from "typeorm";
|
|
||||||
|
|
||||||
import { DirectDiscount } from "../entities/direct-discount.entity";
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class DirectDiscountRepository extends Repository<DirectDiscount> {
|
|
||||||
constructor(@InjectRepository(DirectDiscount) repository: Repository<DirectDiscount>) {
|
|
||||||
super(repository.target, repository.manager, repository.queryRunner);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,8 @@ import { Injectable } from "@nestjs/common";
|
|||||||
import { InjectRepository } from "@nestjs/typeorm";
|
import { InjectRepository } from "@nestjs/typeorm";
|
||||||
import { Repository } from "typeorm";
|
import { Repository } from "typeorm";
|
||||||
|
|
||||||
|
import { PaginationUtils } from "../../utils/providers/pagination.utils";
|
||||||
|
import { SearchDiscountQueryDto } from "../DTO/search-discount-query.dto";
|
||||||
import { Discount } from "../entities/discount.entity";
|
import { Discount } from "../entities/discount.entity";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -13,14 +15,14 @@ export class DiscountRepository extends Repository<Discount> {
|
|||||||
async findById(id: string): Promise<Discount | null> {
|
async findById(id: string): Promise<Discount | null> {
|
||||||
return this.findOne({
|
return this.findOne({
|
||||||
where: { id },
|
where: { id },
|
||||||
relations: ["productRelations"],
|
withDeleted: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async findByCode(code: string): Promise<Discount | null> {
|
async findByCode(code: string): Promise<Discount | null> {
|
||||||
return this.findOne({
|
return this.findOne({
|
||||||
where: { code },
|
where: { code },
|
||||||
relations: ["productRelations"],
|
withDeleted: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,43 +32,27 @@ export class DiscountRepository extends Repository<Discount> {
|
|||||||
code,
|
code,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
},
|
},
|
||||||
relations: ["productRelations"],
|
withDeleted: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async findAll(options?: any): Promise<[Discount[], number]> {
|
async findDiscountForAdmin(queryDto: SearchDiscountQueryDto) {
|
||||||
const query = this.createQueryBuilder("discount").leftJoinAndSelect("discount.productRelations", "productRelations");
|
const { limit, skip } = PaginationUtils(queryDto);
|
||||||
|
const queryBuilder = this.createQueryBuilder("discount")
|
||||||
|
.leftJoin("discount.subscriptionPlans", "plan")
|
||||||
|
.addSelect(["plan.id", "plan.name", "plan.price", "plan.duration"])
|
||||||
|
.leftJoin("plan.service", "service")
|
||||||
|
.addSelect(["service.id", "service.name"])
|
||||||
|
.where("discount.deletedAt IS NULL");
|
||||||
|
|
||||||
if (options?.status) {
|
if (queryDto.q) {
|
||||||
query.andWhere("discount.status = :status", { status: options.status });
|
queryBuilder.andWhere("discount.title ILIKE :query", { query: `%${queryDto.q}%` });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.isActive !== undefined) {
|
return queryBuilder.skip(skip).take(limit).orderBy("discount.createdAt", "DESC").getManyAndCount();
|
||||||
query.andWhere("discount.isActive = :isActive", { isActive: options.isActive });
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (options?.search) {
|
async softDeleteDiscount(id: string) {
|
||||||
query.andWhere("(discount.title ILIKE :search OR discount.code ILIKE :search)", {
|
return this.softDelete(id);
|
||||||
search: `%${options.search}%`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options?.productId) {
|
|
||||||
query.andWhere("EXISTS (SELECT 1 FROM discount_product dp WHERE dp.discountId = discount.id AND dp.productId = :productId)", {
|
|
||||||
productId: options.productId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options?.sort) {
|
|
||||||
const [field, order] = options.sort.split(",");
|
|
||||||
query.orderBy(`discount.${field}`, order.toUpperCase());
|
|
||||||
} else {
|
|
||||||
query.orderBy("discount.createdAt", "DESC");
|
|
||||||
}
|
|
||||||
|
|
||||||
return query
|
|
||||||
.skip(options?.skip || 0)
|
|
||||||
.take(options?.take || 10)
|
|
||||||
.getManyAndCount();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { UserSubscription } from "./user-subscription.entity";
|
|||||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||||
import { DecimalTransformer } from "../../../common/transformers/decimal.transformer";
|
import { DecimalTransformer } from "../../../common/transformers/decimal.transformer";
|
||||||
import { DanakService } from "../../danak-services/entities/danak-service.entity";
|
import { DanakService } from "../../danak-services/entities/danak-service.entity";
|
||||||
import { DirectDiscount } from "../../discounts/entities/direct-discount.entity";
|
import { Discount } from "../../discounts/entities/discount.entity";
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
@Index(["service", "name"], { unique: true })
|
@Index(["service", "name"], { unique: true })
|
||||||
@@ -29,8 +29,8 @@ export class SubscriptionPlan extends BaseEntity {
|
|||||||
@OneToMany(() => UserSubscription, (userSubscription) => userSubscription.plan)
|
@OneToMany(() => UserSubscription, (userSubscription) => userSubscription.plan)
|
||||||
userSubscriptions: UserSubscription[];
|
userSubscriptions: UserSubscription[];
|
||||||
|
|
||||||
@ManyToOne(() => DirectDiscount, (directDiscount) => directDiscount.subscriptionPlans, { nullable: true })
|
@ManyToOne(() => Discount, (discount) => discount.subscriptionPlans, { nullable: true })
|
||||||
directDiscount?: DirectDiscount;
|
directDiscount?: Discount;
|
||||||
|
|
||||||
@DeleteDateColumn({ nullable: true })
|
@DeleteDateColumn({ nullable: true })
|
||||||
deletedAt?: Date;
|
deletedAt?: Date;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { BadRequestException, Injectable } from "@nestjs/common";
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
// eslint-disable-next-line import/no-named-as-default
|
// eslint-disable-next-line import/no-named-as-default
|
||||||
import Decimal from "decimal.js";
|
import Decimal from "decimal.js";
|
||||||
import { DataSource, In, Not } from "typeorm";
|
import { DataSource, In, Not, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
import { ServiceMessage, SubscriptionMessage } from "../../../common/enums/message.enum";
|
import { ServiceMessage, SubscriptionMessage } from "../../../common/enums/message.enum";
|
||||||
import { DanakServicesService } from "../../danak-services/providers/danak-services.service";
|
import { DanakServicesService } from "../../danak-services/providers/danak-services.service";
|
||||||
@@ -269,9 +269,10 @@ export class SubscriptionsService {
|
|||||||
}
|
}
|
||||||
//************************************ */
|
//************************************ */
|
||||||
|
|
||||||
async getAllServiceSubscriptions(serviceIds: string[]) {
|
async getAllServiceSubscriptions(serviceIds: string[], queryRunner: QueryRunner) {
|
||||||
const subscriptions = await this.subscriptionsPlanRepository.find({
|
const subscriptions = await queryRunner.manager.find(this.subscriptionsPlanRepository.target, {
|
||||||
where: { service: { id: In(serviceIds) }, isActive: true },
|
where: { service: { id: In(serviceIds) }, isActive: true },
|
||||||
|
relations: { directDiscount: true, service: true },
|
||||||
});
|
});
|
||||||
return subscriptions;
|
return subscriptions;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user