update: add target service to the announement get by id for admin

This commit is contained in:
mahyargdz
2025-04-17 10:49:57 +03:30
parent f9cb3cf653
commit cd77db93d0
17 changed files with 123 additions and 506 deletions
@@ -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);
}
}