update: fix
This commit is contained in:
@@ -532,6 +532,7 @@ export const enum DiscountMessage {
|
|||||||
DIRECT_DISCOUNT_ALREADY_EXISTS = "تخفیف مستقیم قبلا برای سرویس [serviceName] ثبت شده است",
|
DIRECT_DISCOUNT_ALREADY_EXISTS = "تخفیف مستقیم قبلا برای سرویس [serviceName] ثبت شده است",
|
||||||
UPDATED = "تخفیف با موفقیت بروزرسانی شد",
|
UPDATED = "تخفیف با موفقیت بروزرسانی شد",
|
||||||
DEACTIVATED = "تخفیف با موفقیت غیرفعال شد",
|
DEACTIVATED = "تخفیف با موفقیت غیرفعال شد",
|
||||||
|
TARGET_SERVICES_REQUIRED_FOR_DIRECT_DISCOUNT = "برای تخفیف مستقیم باید حداقل یک سرویس انتخاب شود",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum EmailMessage {
|
export const enum EmailMessage {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export class Discount extends BaseEntity {
|
|||||||
@OneToMany(() => UsageDiscount, (usageDiscount) => usageDiscount.discount)
|
@OneToMany(() => UsageDiscount, (usageDiscount) => usageDiscount.discount)
|
||||||
usages: UsageDiscount[];
|
usages: UsageDiscount[];
|
||||||
|
|
||||||
@OneToMany(() => SubscriptionPlan, (subscriptionPlan) => subscriptionPlan.directDiscount)
|
@OneToMany(() => SubscriptionPlan, (subscriptionPlan) => subscriptionPlan.directDiscount, { cascade: ["update"] })
|
||||||
subscriptionPlans: SubscriptionPlan[];
|
subscriptionPlans: SubscriptionPlan[];
|
||||||
|
|
||||||
@OneToMany(() => Invoice, (invoice) => invoice.discount)
|
@OneToMany(() => Invoice, (invoice) => invoice.discount)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import dayjs from "dayjs";
|
|||||||
import { Decimal } from "decimal.js";
|
import { Decimal } from "decimal.js";
|
||||||
import { DataSource, QueryRunner } from "typeorm";
|
import { DataSource, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
import { DiscountMessage } from "../../../common/enums/message.enum";
|
import { CommonMessage, DiscountMessage } from "../../../common/enums/message.enum";
|
||||||
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
|
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
|
||||||
import { SubscriptionsService } from "../../subscriptions/providers/subscriptions.service";
|
import { SubscriptionsService } from "../../subscriptions/providers/subscriptions.service";
|
||||||
import { DISCOUNT } from "../constants";
|
import { DISCOUNT } from "../constants";
|
||||||
@@ -30,6 +30,13 @@ export class DiscountsService {
|
|||||||
private readonly dataSource: DataSource,
|
private readonly dataSource: DataSource,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
//======================================
|
||||||
|
// PUBLIC METHODS
|
||||||
|
//======================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new discount
|
||||||
|
*/
|
||||||
async create(createDiscountDto: CreateDiscountDto) {
|
async create(createDiscountDto: CreateDiscountDto) {
|
||||||
const queryRunner = this.dataSource.createQueryRunner();
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
|
|
||||||
@@ -55,7 +62,10 @@ export class DiscountsService {
|
|||||||
await queryRunner.release();
|
await queryRunner.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//************************************ */
|
|
||||||
|
/**
|
||||||
|
* Get all discounts for admin panel with pagination
|
||||||
|
*/
|
||||||
async getDiscountsForAdmin(queryDto: SearchDiscountQueryDto) {
|
async getDiscountsForAdmin(queryDto: SearchDiscountQueryDto) {
|
||||||
const [discounts, count] = await this.discountRepository.findDiscountForAdmin(queryDto);
|
const [discounts, count] = await this.discountRepository.findDiscountForAdmin(queryDto);
|
||||||
|
|
||||||
@@ -65,12 +75,30 @@ export class DiscountsService {
|
|||||||
pagination: true,
|
pagination: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
//************************************ */
|
|
||||||
async getDiscountById(id: string) {
|
|
||||||
return this.discountRepository.findById(id);
|
|
||||||
}
|
|
||||||
//************************************ */
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a discount by ID
|
||||||
|
*/
|
||||||
|
async getDiscountById(id: string) {
|
||||||
|
const discount = await this.discountRepository.findById(id);
|
||||||
|
if (!discount) throw new BadRequestException(DiscountMessage.NOT_FOUND);
|
||||||
|
|
||||||
|
return { discount };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a discount with distinct services for client
|
||||||
|
*/
|
||||||
|
async getDiscountWithDistinctServicesForClient(id: string) {
|
||||||
|
const discount = await this.discountRepository.findDiscountWithDistinctServicesForClient(id);
|
||||||
|
if (!discount) throw new BadRequestException(DiscountMessage.NOT_FOUND);
|
||||||
|
|
||||||
|
return { discount };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle discount active status
|
||||||
|
*/
|
||||||
async toggleDiscountStatus(id: string) {
|
async toggleDiscountStatus(id: string) {
|
||||||
const discount = await this.discountRepository.findById(id);
|
const discount = await this.discountRepository.findById(id);
|
||||||
if (!discount) throw new BadRequestException(DiscountMessage.NOT_FOUND);
|
if (!discount) throw new BadRequestException(DiscountMessage.NOT_FOUND);
|
||||||
@@ -83,7 +111,10 @@ export class DiscountsService {
|
|||||||
discount,
|
discount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
//************************************ */
|
|
||||||
|
/**
|
||||||
|
* Update an existing discount
|
||||||
|
*/
|
||||||
async updateDiscount(id: string, updateDiscountDto: UpdateDiscountDto) {
|
async updateDiscount(id: string, updateDiscountDto: UpdateDiscountDto) {
|
||||||
const queryRunner = this.dataSource.createQueryRunner();
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
|
|
||||||
@@ -101,23 +132,42 @@ export class DiscountsService {
|
|||||||
await this.addDiscountDeactivationJob(existingDiscount.id, updateDiscountDto.endDate);
|
await this.addDiscountDeactivationJob(existingDiscount.id, updateDiscountDto.endDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updateDiscountDto.value) existingDiscount.value = updateDiscountDto.value;
|
// Check if discount is being converted from code-based to direct
|
||||||
|
const isConvertingToDirectDiscount =
|
||||||
|
updateDiscountDto.direct === true && existingDiscount.applicationType === DiscountApplicationType.CODE_BASED;
|
||||||
|
|
||||||
await queryRunner.manager.save({
|
// When converting to direct discount, target services are required
|
||||||
...existingDiscount,
|
if (isConvertingToDirectDiscount && (!updateDiscountDto.targetServices || updateDiscountDto.targetServices.length === 0)) {
|
||||||
|
throw new BadRequestException(DiscountMessage.TARGET_SERVICES_REQUIRED_FOR_DIRECT_DISCOUNT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal direct discount update or first-time conversion to direct discount with services
|
||||||
|
if (updateDiscountDto.direct && updateDiscountDto.targetServices && updateDiscountDto.targetServices.length > 0) {
|
||||||
|
await this.updateDirectDiscount(updateDiscountDto, existingDiscount, queryRunner);
|
||||||
|
}
|
||||||
|
// When changing from direct to code-based, clear all associated plans
|
||||||
|
else if (updateDiscountDto.direct === false && existingDiscount.applicationType === DiscountApplicationType.DIRECT) {
|
||||||
|
await this.clearDirectDiscountFromPlans(existingDiscount, queryRunner);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update discount properties directly on existingDiscount to preserve relationships
|
||||||
|
// This is done AFTER handling subscription plans to prevent overwriting relationship changes
|
||||||
|
Object.assign(existingDiscount, {
|
||||||
...updateDiscountDto,
|
...updateDiscountDto,
|
||||||
applicationType: updateDiscountDto.direct ? DiscountApplicationType.DIRECT : DiscountApplicationType.CODE_BASED,
|
applicationType: updateDiscountDto.direct ? DiscountApplicationType.DIRECT : DiscountApplicationType.CODE_BASED,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (updateDiscountDto.direct) {
|
// Save the updated discount
|
||||||
await this.updateDirectDiscount(updateDiscountDto, existingDiscount, queryRunner);
|
await queryRunner.manager.save(this.discountRepository.target, existingDiscount);
|
||||||
}
|
|
||||||
|
|
||||||
await queryRunner.commitTransaction();
|
await queryRunner.commitTransaction();
|
||||||
|
|
||||||
|
// Get the updated discount with all relations
|
||||||
|
const updatedDiscount = await this.discountRepository.findById(id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: DiscountMessage.UPDATED,
|
message: DiscountMessage.UPDATED,
|
||||||
discount: existingDiscount,
|
discount: updatedDiscount,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await queryRunner.rollbackTransaction();
|
await queryRunner.rollbackTransaction();
|
||||||
@@ -126,12 +176,21 @@ export class DiscountsService {
|
|||||||
await queryRunner.release();
|
await queryRunner.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//************************************ */
|
|
||||||
async softDeleteDiscount(id: string): Promise<void> {
|
|
||||||
await this.discountRepository.softDeleteDiscount(id);
|
|
||||||
}
|
|
||||||
//************************************ */
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Soft delete a discount
|
||||||
|
*/
|
||||||
|
async softDeleteDiscount(id: string) {
|
||||||
|
await this.discountRepository.softDeleteDiscount(id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: CommonMessage.DELETED,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deactivate a discount and update related subscription plans
|
||||||
|
*/
|
||||||
async deactivateDiscount(id: string, queryRunner: QueryRunner) {
|
async deactivateDiscount(id: string, queryRunner: QueryRunner) {
|
||||||
const discount = await queryRunner.manager.findOne(this.discountRepository.target, {
|
const discount = await queryRunner.manager.findOne(this.discountRepository.target, {
|
||||||
where: { id, isActive: true },
|
where: { id, isActive: true },
|
||||||
@@ -143,16 +202,14 @@ export class DiscountsService {
|
|||||||
if (!discount) throw new BadRequestException(DiscountMessage.NOT_FOUND);
|
if (!discount) throw new BadRequestException(DiscountMessage.NOT_FOUND);
|
||||||
|
|
||||||
discount.isActive = false;
|
discount.isActive = false;
|
||||||
await queryRunner.manager.save(discount);
|
await queryRunner.manager.save(this.discountRepository.target, discount);
|
||||||
const subscriptionPlans = discount.subscriptionPlans;
|
const subscriptionPlans = discount.subscriptionPlans;
|
||||||
|
|
||||||
if (discount.applicationType === DiscountApplicationType.DIRECT) {
|
if (discount.applicationType === DiscountApplicationType.DIRECT) {
|
||||||
for (const subscriptionPlan of subscriptionPlans) {
|
for (const subscriptionPlan of subscriptionPlans) {
|
||||||
subscriptionPlan.directDiscount = undefined;
|
subscriptionPlan.directDiscount = null!;
|
||||||
subscriptionPlan.price = subscriptionPlan.originalPrice
|
subscriptionPlan.price = new Decimal(subscriptionPlan.originalPrice);
|
||||||
? new Decimal(subscriptionPlan.originalPrice)
|
await queryRunner.manager.save(SubscriptionPlan, subscriptionPlan);
|
||||||
: new Decimal(subscriptionPlan.price);
|
|
||||||
await queryRunner.manager.save(subscriptionPlan);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +218,14 @@ export class DiscountsService {
|
|||||||
discount,
|
discount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
//************************************ */
|
|
||||||
|
//======================================
|
||||||
|
// PRIVATE METHODS
|
||||||
|
//======================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a discount entity and handle code generation or direct discount application
|
||||||
|
*/
|
||||||
private async createDiscount(createDiscountDto: CreateDiscountDto, queryRunner: QueryRunner) {
|
private async createDiscount(createDiscountDto: CreateDiscountDto, queryRunner: QueryRunner) {
|
||||||
const discount = queryRunner.manager.create(this.discountRepository.target, {
|
const discount = queryRunner.manager.create(this.discountRepository.target, {
|
||||||
...createDiscountDto,
|
...createDiscountDto,
|
||||||
@@ -173,7 +237,7 @@ export class DiscountsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
discount.value = this.calculateDiscountValue(createDiscountDto);
|
discount.value = this.calculateDiscountValue(createDiscountDto);
|
||||||
await queryRunner.manager.save(discount);
|
await queryRunner.manager.save(this.discountRepository.target, discount);
|
||||||
|
|
||||||
if (createDiscountDto.direct) {
|
if (createDiscountDto.direct) {
|
||||||
await this.applyDirectDiscount(createDiscountDto, discount, queryRunner);
|
await this.applyDirectDiscount(createDiscountDto, discount, queryRunner);
|
||||||
@@ -181,7 +245,10 @@ export class DiscountsService {
|
|||||||
|
|
||||||
return discount;
|
return discount;
|
||||||
}
|
}
|
||||||
//************************************ */
|
|
||||||
|
/**
|
||||||
|
* Apply a direct discount to subscription plans
|
||||||
|
*/
|
||||||
private async applyDirectDiscount(createDiscountDto: CreateDiscountDto, discount: Discount, queryRunner: QueryRunner) {
|
private async applyDirectDiscount(createDiscountDto: CreateDiscountDto, discount: Discount, queryRunner: QueryRunner) {
|
||||||
const subscriptionPlans = await this.subscriptionService.getAllServiceSubscriptions(
|
const subscriptionPlans = await this.subscriptionService.getAllServiceSubscriptions(
|
||||||
createDiscountDto.targetServices ?? [],
|
createDiscountDto.targetServices ?? [],
|
||||||
@@ -195,16 +262,120 @@ export class DiscountsService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate prices for all subscription plans
|
||||||
for (const subscriptionPlan of subscriptionPlans) {
|
for (const subscriptionPlan of subscriptionPlans) {
|
||||||
subscriptionPlan.directDiscount = discount;
|
if (!subscriptionPlan.originalPrice) {
|
||||||
await this.updatePlanPrice(subscriptionPlan, discount);
|
subscriptionPlan.originalPrice = new Decimal(subscriptionPlan.price);
|
||||||
await queryRunner.manager.save(subscriptionPlan);
|
}
|
||||||
|
|
||||||
|
let discountAmount: Decimal;
|
||||||
|
if (discount.type === DiscountType.PERCENTAGE) {
|
||||||
|
discountAmount = new Decimal(subscriptionPlan.originalPrice).mul(discount.value).div(100);
|
||||||
|
} else {
|
||||||
|
discountAmount = new Decimal(discount.value);
|
||||||
|
}
|
||||||
|
subscriptionPlan.price = new Decimal(subscriptionPlan.originalPrice).sub(discountAmount);
|
||||||
}
|
}
|
||||||
|
|
||||||
await queryRunner.manager.save(discount);
|
// Update the discount and set subscription plans in one operation
|
||||||
|
discount.subscriptionPlans = subscriptionPlans;
|
||||||
|
await queryRunner.manager.save(this.discountRepository.target, discount);
|
||||||
}
|
}
|
||||||
//************************************ */
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a direct discount's subscription plans
|
||||||
|
*/
|
||||||
|
private async updateDirectDiscount(updateDiscountDto: UpdateDiscountDto, discount: Discount, queryRunner: QueryRunner) {
|
||||||
|
// Get all subscription plans for the target services
|
||||||
|
const subscriptionPlans = await this.subscriptionService.getAllServiceSubscriptions(
|
||||||
|
updateDiscountDto.targetServices ?? [],
|
||||||
|
queryRunner,
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`Found ${subscriptionPlans.length} subscription plans for target services`);
|
||||||
|
|
||||||
|
// Get current plans with direct discount regardless of loaded relations
|
||||||
|
let currentPlans: SubscriptionPlan[] = [];
|
||||||
|
if (discount.applicationType === DiscountApplicationType.DIRECT) {
|
||||||
|
// Find all subscription plans that have this discount directly applied
|
||||||
|
currentPlans = await queryRunner.manager.find(SubscriptionPlan, {
|
||||||
|
where: { directDiscount: { id: discount.id } },
|
||||||
|
relations: { directDiscount: true, service: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Found ${currentPlans.length} current plans with this discount applied`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove discount from plans that are no longer targeted
|
||||||
|
for (const plan of currentPlans) {
|
||||||
|
if (!subscriptionPlans.some((newPlan) => newPlan.id === plan.id)) {
|
||||||
|
console.log(`Removing discount from plan ${plan.id}`);
|
||||||
|
plan.directDiscount = null!;
|
||||||
|
plan.price = new Decimal(plan.originalPrice);
|
||||||
|
await queryRunner.manager.save(SubscriptionPlan, plan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for existing discounts on new 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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate prices for all subscription plans
|
||||||
|
for (const subscriptionPlan of subscriptionPlans) {
|
||||||
|
if (!subscriptionPlan.originalPrice) {
|
||||||
|
subscriptionPlan.originalPrice = new Decimal(subscriptionPlan.price);
|
||||||
|
}
|
||||||
|
|
||||||
|
let discountAmount: Decimal;
|
||||||
|
if (discount.type === DiscountType.PERCENTAGE) {
|
||||||
|
discountAmount = new Decimal(subscriptionPlan.originalPrice).mul(discount.value).div(100);
|
||||||
|
} else {
|
||||||
|
discountAmount = new Decimal(discount.value);
|
||||||
|
}
|
||||||
|
subscriptionPlan.price = new Decimal(subscriptionPlan.originalPrice).sub(discountAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Setting ${subscriptionPlans.length} subscription plans on discount ${discount.id}`);
|
||||||
|
|
||||||
|
// Update the discount and set subscription plans in one operation
|
||||||
|
discount.subscriptionPlans = subscriptionPlans;
|
||||||
|
|
||||||
|
// First save each subscription plan with explicit reference to the discount
|
||||||
|
for (const plan of subscriptionPlans) {
|
||||||
|
plan.directDiscount = discount;
|
||||||
|
await queryRunner.manager.save(SubscriptionPlan, plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then save the discount with the relationship
|
||||||
|
await queryRunner.manager.save(this.discountRepository.target, discount);
|
||||||
|
|
||||||
|
return subscriptionPlans;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear direct discount from plans
|
||||||
|
*/
|
||||||
|
private async clearDirectDiscountFromPlans(discount: Discount, queryRunner: QueryRunner) {
|
||||||
|
const subscriptionPlans = await queryRunner.manager.find(SubscriptionPlan, {
|
||||||
|
where: { directDiscount: { id: discount.id } },
|
||||||
|
relations: { directDiscount: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const plan of subscriptionPlans) {
|
||||||
|
plan.directDiscount = null!;
|
||||||
|
plan.price = new Decimal(plan.originalPrice);
|
||||||
|
await queryRunner.manager.save(SubscriptionPlan, plan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a discount code for code-based discounts
|
||||||
|
*/
|
||||||
private async generateDiscountCode(discount: Discount, queryRunner: QueryRunner) {
|
private async generateDiscountCode(discount: Discount, queryRunner: QueryRunner) {
|
||||||
let attempts = 0;
|
let attempts = 0;
|
||||||
let code: string;
|
let code: string;
|
||||||
@@ -220,28 +391,37 @@ export class DiscountsService {
|
|||||||
|
|
||||||
discount.code = code;
|
discount.code = code;
|
||||||
}
|
}
|
||||||
//************************************ */
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random discount code
|
||||||
|
*/
|
||||||
|
private async generateRandomCode(length: number = 4) {
|
||||||
|
return randomBytes(length).toString("hex").toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a discount by code
|
||||||
|
*/
|
||||||
private async findDiscountByCode(code: string, queryRunner: QueryRunner) {
|
private async findDiscountByCode(code: string, queryRunner: QueryRunner) {
|
||||||
return queryRunner.manager.findOne(this.discountRepository.target, {
|
return queryRunner.manager.findOne(this.discountRepository.target, {
|
||||||
where: { code, isActive: true },
|
where: { code, isActive: true },
|
||||||
withDeleted: false,
|
withDeleted: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
//************************************ */
|
|
||||||
|
|
||||||
private async generateRandomCode(length: number = 4) {
|
|
||||||
return randomBytes(length).toString("hex").toUpperCase();
|
|
||||||
}
|
|
||||||
//************************************ */
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate discount value based on discount type
|
||||||
|
*/
|
||||||
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;
|
||||||
if (createDiscountDto.type === DiscountType.FIXED_AMOUNT) return createDiscountDto.value;
|
if (createDiscountDto.type === DiscountType.FIXED_AMOUNT) return createDiscountDto.value;
|
||||||
|
|
||||||
throw new BadRequestException(DiscountMessage.INVALID_DISCOUNT_TYPE);
|
throw new BadRequestException(DiscountMessage.INVALID_DISCOUNT_TYPE);
|
||||||
}
|
}
|
||||||
//************************************ */
|
|
||||||
|
/**
|
||||||
|
* Validate discount start and end dates
|
||||||
|
*/
|
||||||
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);
|
||||||
@@ -259,60 +439,19 @@ 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: UpdateDiscountDto, discount: Discount, queryRunner: QueryRunner) {
|
|
||||||
const subscriptionPlans = await this.subscriptionService.getAllServiceSubscriptions(
|
|
||||||
updateDiscountDto.targetServices ?? [],
|
|
||||||
queryRunner,
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentPlans = await this.subscriptionService.getAllServiceSubscriptions(
|
/**
|
||||||
discount.subscriptionPlans.map((plan) => plan.service.id),
|
* Remove discount deactivation job
|
||||||
queryRunner,
|
*/
|
||||||
);
|
|
||||||
|
|
||||||
for (const plan of currentPlans) {
|
|
||||||
if (!subscriptionPlans.some((newPlan) => newPlan.id === plan.id)) {
|
|
||||||
plan.directDiscount = undefined;
|
|
||||||
await queryRunner.manager.save(plan);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const subscriptionPlan of subscriptionPlans) {
|
|
||||||
subscriptionPlan.directDiscount = discount;
|
|
||||||
await this.updatePlanPrice(subscriptionPlan, discount);
|
|
||||||
await queryRunner.manager.save(subscriptionPlan);
|
|
||||||
}
|
|
||||||
|
|
||||||
await queryRunner.manager.save(discount);
|
|
||||||
}
|
|
||||||
//************************************ */
|
|
||||||
private async updatePlanPrice(subscriptionPlan: SubscriptionPlan, discount: Discount) {
|
|
||||||
if (!subscriptionPlan.originalPrice) subscriptionPlan.originalPrice = new Decimal(subscriptionPlan.price);
|
|
||||||
|
|
||||||
let discountAmount: Decimal;
|
|
||||||
if (discount.type === DiscountType.PERCENTAGE) {
|
|
||||||
discountAmount = subscriptionPlan.originalPrice.mul(discount.value).div(100);
|
|
||||||
} else {
|
|
||||||
discountAmount = new Decimal(discount.value);
|
|
||||||
}
|
|
||||||
subscriptionPlan.price = subscriptionPlan.originalPrice.sub(discountAmount);
|
|
||||||
}
|
|
||||||
//************************************ */
|
|
||||||
private async removeDiscountDeactivationJob(discountId: string) {
|
private async removeDiscountDeactivationJob(discountId: string) {
|
||||||
const jobs = await this.discountQueue.getJobs(["delayed", "waiting", "active"]);
|
const jobs = await this.discountQueue.getJobs(["delayed", "waiting", "active"]);
|
||||||
const jobToRemove = jobs.find((job) => job.name === DISCOUNT.DISCOUNT_DEACTIVATE_JOB_NAME && job.data.discountId === discountId);
|
const jobToRemove = jobs.find((job) => job.name === DISCOUNT.DISCOUNT_DEACTIVATE_JOB_NAME && job.data.discountId === discountId);
|
||||||
if (jobToRemove) await jobToRemove.remove();
|
if (jobToRemove) await jobToRemove.remove();
|
||||||
}
|
}
|
||||||
//************************************ */
|
|
||||||
|
/**
|
||||||
|
* Add discount deactivation job
|
||||||
|
*/
|
||||||
private async addDiscountDeactivationJob(discountId: string, endDate: string) {
|
private async addDiscountDeactivationJob(discountId: string, endDate: string) {
|
||||||
return this.discountQueue.add(
|
return this.discountQueue.add(
|
||||||
DISCOUNT.DISCOUNT_DEACTIVATE_JOB_NAME,
|
DISCOUNT.DISCOUNT_DEACTIVATE_JOB_NAME,
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ export class DiscountRepository extends Repository<Discount> {
|
|||||||
return this.findOne({
|
return this.findOne({
|
||||||
where: { id },
|
where: { id },
|
||||||
relations: {
|
relations: {
|
||||||
subscriptionPlans: true,
|
subscriptionPlans: {
|
||||||
|
service: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
withDeleted: false,
|
withDeleted: false,
|
||||||
});
|
});
|
||||||
@@ -58,4 +60,51 @@ export class DiscountRepository extends Repository<Discount> {
|
|||||||
async softDeleteDiscount(id: string) {
|
async softDeleteDiscount(id: string) {
|
||||||
return this.softDelete(id);
|
return this.softDelete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findDiscountWithDistinctServicesForClient(id: string) {
|
||||||
|
// First, get the basic discount information
|
||||||
|
const discount = await this.findOne({
|
||||||
|
where: { id, isActive: true },
|
||||||
|
withDeleted: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!discount) return null;
|
||||||
|
|
||||||
|
// Get subscription plans with their services
|
||||||
|
const queryBuilder = this.createQueryBuilder("discount")
|
||||||
|
.leftJoinAndSelect("discount.subscriptionPlans", "plans")
|
||||||
|
.leftJoinAndSelect("plans.service", "service")
|
||||||
|
.where("discount.id = :id", { id })
|
||||||
|
.andWhere("discount.isActive = :isActive", { isActive: true });
|
||||||
|
|
||||||
|
const discountWithPlans = await queryBuilder.getOne();
|
||||||
|
|
||||||
|
// Get distinct services using raw SQL
|
||||||
|
const distinctServicesQuery = this.manager.query(
|
||||||
|
`
|
||||||
|
SELECT DISTINCT ON (service.id)
|
||||||
|
service.id,
|
||||||
|
service.name,
|
||||||
|
service.description,
|
||||||
|
service.icon_url as "iconUrl",
|
||||||
|
service.is_active as "isActive"
|
||||||
|
FROM danak_service service
|
||||||
|
INNER JOIN subscription_plan plan ON plan.service_id = service.id
|
||||||
|
INNER JOIN discount_subscription_plans dsp ON dsp.subscription_plan_id = plan.id
|
||||||
|
WHERE dsp.discount_id = $1
|
||||||
|
AND service.deleted_at IS NULL
|
||||||
|
AND service.is_active = true
|
||||||
|
`,
|
||||||
|
[id],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Combine the results
|
||||||
|
const [distinctServices] = await Promise.all([distinctServicesQuery]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...discount,
|
||||||
|
subscriptionPlans: discountWithPlans?.subscriptionPlans || [],
|
||||||
|
distinctServices,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ export class SubscriptionPlan extends BaseEntity {
|
|||||||
@Column({ type: "decimal", precision: 16, scale: 2, nullable: false, transformer: new DecimalTransformer() })
|
@Column({ type: "decimal", precision: 16, scale: 2, nullable: false, transformer: new DecimalTransformer() })
|
||||||
price: Decimal;
|
price: Decimal;
|
||||||
|
|
||||||
@Column({ type: "decimal", precision: 16, scale: 2, nullable: true, transformer: new DecimalTransformer() })
|
@Column({ type: "decimal", precision: 16, scale: 2, nullable: false, transformer: new DecimalTransformer() })
|
||||||
originalPrice?: Decimal;
|
originalPrice: Decimal;
|
||||||
|
|
||||||
@Column({ type: "boolean", default: true })
|
@Column({ type: "boolean", default: true })
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
@@ -32,7 +32,7 @@ export class SubscriptionPlan extends BaseEntity {
|
|||||||
@OneToMany(() => UserSubscription, (userSubscription) => userSubscription.plan)
|
@OneToMany(() => UserSubscription, (userSubscription) => userSubscription.plan)
|
||||||
userSubscriptions: UserSubscription[];
|
userSubscriptions: UserSubscription[];
|
||||||
|
|
||||||
@ManyToOne(() => Discount, (discount) => discount.subscriptionPlans, { nullable: true })
|
@ManyToOne(() => Discount, (discount) => discount.subscriptionPlans, { nullable: true, onUpdate: "CASCADE" })
|
||||||
directDiscount?: Discount;
|
directDiscount?: Discount;
|
||||||
|
|
||||||
@DeleteDateColumn({ nullable: true })
|
@DeleteDateColumn({ nullable: true })
|
||||||
|
|||||||
@@ -37,7 +37,11 @@ export class SubscriptionsService {
|
|||||||
const danakService = await this.danakServices.findServiceById(createDto.serviceId);
|
const danakService = await this.danakServices.findServiceById(createDto.serviceId);
|
||||||
if (!danakService) throw new BadRequestException(ServiceMessage.SERVICE_NOT_FOUND_BY_ID);
|
if (!danakService) throw new BadRequestException(ServiceMessage.SERVICE_NOT_FOUND_BY_ID);
|
||||||
|
|
||||||
const subscriptions = createDto.subs.map((sub) => ({ ...sub, service: { id: createDto.serviceId } }));
|
const subscriptions = createDto.subs.map((sub) => ({
|
||||||
|
...sub,
|
||||||
|
service: { id: createDto.serviceId },
|
||||||
|
originalPrice: new Decimal(sub.price),
|
||||||
|
}));
|
||||||
|
|
||||||
const subscriptionNames = createDto.subs.map((sub) => sub.name);
|
const subscriptionNames = createDto.subs.map((sub) => sub.name);
|
||||||
const existingSubscriptions = await this.subscriptionsPlanRepository.find({
|
const existingSubscriptions = await this.subscriptionsPlanRepository.find({
|
||||||
@@ -124,7 +128,10 @@ export class SubscriptionsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (updateDto.duration) subscription.duration = updateDto.duration;
|
if (updateDto.duration) subscription.duration = updateDto.duration;
|
||||||
if (updateDto.price) subscription.price = new Decimal(updateDto.price);
|
if (updateDto.price) {
|
||||||
|
subscription.price = new Decimal(updateDto.price);
|
||||||
|
subscription.originalPrice = new Decimal(updateDto.price);
|
||||||
|
}
|
||||||
|
|
||||||
await this.subscriptionsPlanRepository.save(subscription);
|
await this.subscriptionsPlanRepository.save(subscription);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user