Files
dsc-api/src/modules/discounts/providers/discounts.service.ts
T

257 lines
9.2 KiB
TypeScript
Executable File

import { randomBytes } from "node:crypto";
import { BadRequestException, Injectable } from "@nestjs/common";
import dayjs from "dayjs";
import { Decimal } from "decimal.js";
import { DataSource, QueryRunner } from "typeorm";
import { DiscountMessage } from "../../../common/enums/message.enum";
import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity";
import { SubscriptionsService } from "../../subscriptions/providers/subscriptions.service";
import { CreateDiscountDto } from "../DTO/create-discount.dto";
import { SearchDiscountQueryDto } from "../DTO/search-discount-query.dto";
import { UpdateDiscountDto } from "../DTO/update-discount.dto";
import { Discount } from "../entities/discount.entity";
import { DiscountApplicationType } from "../enums/discount-application-type.enum";
import { DiscountType } from "../enums/discount-type.enum";
import { DiscountRepository } from "../repositories/discounts.repository";
@Injectable()
export class DiscountsService {
private MAX_ATTEMPTS = 10;
constructor(
private readonly discountRepository: DiscountRepository,
private readonly subscriptionService: SubscriptionsService,
private readonly dataSource: DataSource,
) {}
async create(createDiscountDto: CreateDiscountDto) {
const queryRunner = this.dataSource.createQueryRunner();
try {
await queryRunner.connect();
await queryRunner.startTransaction();
await this.validateDates(createDiscountDto.startDate, createDiscountDto.endDate);
const discount = await this.createDiscount(createDiscountDto, queryRunner);
await queryRunner.commitTransaction();
return {
message: DiscountMessage.CREATED,
discount,
};
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
}
//************************************ */
async getDiscountsForAdmin(queryDto: SearchDiscountQueryDto) {
const [discounts, count] = await this.discountRepository.findDiscountForAdmin(queryDto);
return {
discounts,
count,
pagination: true,
};
}
//************************************ */
async getDiscountById(id: string) {
return this.discountRepository.findById(id);
}
//************************************ */
async updateDiscount(id: string, updateDiscountDto: UpdateDiscountDto) {
const queryRunner = this.dataSource.createQueryRunner();
try {
await queryRunner.connect();
await queryRunner.startTransaction();
const existingDiscount = await this.discountRepository.findById(id);
if (!existingDiscount) throw new BadRequestException(DiscountMessage.NOT_FOUND);
if (updateDiscountDto.startDate && updateDiscountDto.endDate)
await this.validateDates(updateDiscountDto.startDate, updateDiscountDto.endDate);
if (updateDiscountDto.value) existingDiscount.value = updateDiscountDto.value;
await queryRunner.manager.save({
...existingDiscount,
...updateDiscountDto,
applicationType: updateDiscountDto.direct ? DiscountApplicationType.DIRECT : DiscountApplicationType.CODE_BASED,
});
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 this.updatePlanPrice(subscriptionPlan, discount);
await queryRunner.manager.save(subscriptionPlan);
}
await queryRunner.manager.save(discount);
}
//************************************ */
private async generateDiscountCode(discount: Discount, queryRunner: QueryRunner) {
let attempts = 0;
let code: string;
let existCode;
do {
if (attempts >= this.MAX_ATTEMPTS) throw new BadRequestException(DiscountMessage.CODE_GENERATION_FAILED);
code = await this.generateRandomCode();
existCode = await this.findDiscountByCode(code, queryRunner);
attempts++;
} while (existCode && attempts < this.MAX_ATTEMPTS);
discount.code = code;
}
//************************************ */
private async findDiscountByCode(code: string, queryRunner: QueryRunner) {
return queryRunner.manager.findOne(this.discountRepository.target, {
where: { code },
withDeleted: false,
});
}
//************************************ */
private async generateRandomCode(length: number = 4) {
return randomBytes(length).toString("hex").toUpperCase();
}
//************************************ */
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);
}
//************************************ */
private async validateDates(startDate: string, endDate: string) {
if (dayjs(startDate).isAfter(dayjs(endDate))) {
throw new BadRequestException(DiscountMessage.START_DATE_MUST_BE_BEFORE_END_DATE);
}
if (dayjs(startDate).isBefore(dayjs().subtract(1, "day"))) {
throw new BadRequestException(DiscountMessage.START_DATE_MUST_BE_AFTER_NOW);
}
if (dayjs(startDate).isSame(dayjs(endDate))) {
throw new BadRequestException(DiscountMessage.START_DATE_MUST_BE_BEFORE_END_DATE);
}
if (dayjs(endDate).isBefore(dayjs())) {
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),
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);
}
}