fix: bug
This commit is contained in:
@@ -1,194 +1,326 @@
|
||||
// import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
// // eslint-disable-next-line import/no-named-as-default
|
||||
// import Decimal from "decimal.js";
|
||||
import { randomBytes } from "node:crypto";
|
||||
|
||||
// import { User } from "../../users/entities/user.entity";
|
||||
// import { CreateDiscountDto } from "../DTO/create-discount.dto";
|
||||
// import { UpdateDiscountDto } from "../DTO/update-discount.dto";
|
||||
// import { Discount } from "../entities/discount.entity";
|
||||
// import { DiscountType } from "../enums/discount-type.enum";
|
||||
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 { 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 { DiscountRepository } from "../repositories/discounts.repository";
|
||||
import { UsageDiscountRepository } from "../repositories/usage-discounts.repository";
|
||||
import { DiscountValidator } from "../utils/discount-validator";
|
||||
|
||||
// @Injectable()
|
||||
// export class DiscountsService {
|
||||
// constructor(
|
||||
// private readonly discountRepository: DiscountRepository,
|
||||
// private readonly discountServicesRepository: DiscountServicesRepository,
|
||||
// private readonly usageDiscountRepository: UsageDiscountRepository,
|
||||
// ) {}
|
||||
@Injectable()
|
||||
export class DiscountsService {
|
||||
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 existingDiscount = await this.discountRepository.findByCode(createDiscountDto.title);
|
||||
// if (existingDiscount) {
|
||||
// throw new BadRequestException(DISCOUNT_ERROR_MESSAGES.ALREADY_EXISTS);
|
||||
// }
|
||||
async create(createDiscountDto: CreateDiscountDto) {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
const { direct } = createDiscountDto;
|
||||
|
||||
// // Create the discount without product relations
|
||||
// const { targetProducts, ...discountData } = createDiscountDto;
|
||||
try {
|
||||
await queryRunner.connect();
|
||||
await queryRunner.startTransaction();
|
||||
|
||||
// const discount = await this.discountRepository.create({
|
||||
// ...discountData,
|
||||
// startDate: new Date(createDiscountDto.startDate),
|
||||
// endDate: new Date(createDiscountDto.endDate),
|
||||
// status: createDiscountDto.status || DiscountStatus.ACTIVE,
|
||||
// isActive: createDiscountDto.isActive !== undefined ? createDiscountDto.isActive : true,
|
||||
// });
|
||||
const discount = queryRunner.manager.create(this.discountRepository.target, createDiscountDto);
|
||||
|
||||
// // Add product relations if provided
|
||||
// if (targetProducts && targetProducts.length > 0) {
|
||||
// await this.discountServicesRepository.createMany(discount.id, targetProducts);
|
||||
// }
|
||||
if (!direct) {
|
||||
const code = await this.generateDiscountCode();
|
||||
discount.code = code;
|
||||
}
|
||||
|
||||
// // Fetch the complete discount with relations
|
||||
// const fullDiscount = await this.discountRepository.findById(discount.id);
|
||||
// if (!fullDiscount) {
|
||||
// throw new NotFoundException(DISCOUNT_ERROR_MESSAGES.NOT_FOUND);
|
||||
// }
|
||||
// return fullDiscount;
|
||||
// }
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
// async findAll(options?: any): Promise<[Discount[], number]> {
|
||||
// return this.discountRepository.findAll(options);
|
||||
// }
|
||||
return {
|
||||
message: DiscountMessage.CREATED,
|
||||
};
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
// async findById(id: string): Promise<Discount> {
|
||||
// const discount = await this.discountRepository.findById(id);
|
||||
// if (!discount) {
|
||||
// throw new NotFoundException(DISCOUNT_ERROR_MESSAGES.NOT_FOUND);
|
||||
// }
|
||||
// return discount;
|
||||
// }
|
||||
async findById(id: string) {
|
||||
const discount = await this.discountRepository.findById(id);
|
||||
|
||||
// async update(id: string, updateDiscountDto: UpdateDiscountDto): Promise<Discount> {
|
||||
// // Validate discount exists
|
||||
// await this.findById(id);
|
||||
if (!discount) throw new BadRequestException(DiscountMessage.NOT_FOUND);
|
||||
|
||||
// // Handle product relations separately
|
||||
// const { targetProducts, startDate: startDateStr, endDate: endDateStr, ...otherFields } = updateDiscountDto;
|
||||
return { discount };
|
||||
}
|
||||
|
||||
// // Initialize update data with other fields
|
||||
// const updateData: Partial<Discount> = { ...otherFields };
|
||||
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"],
|
||||
});
|
||||
|
||||
// // Add date fields if they exist
|
||||
// if (startDateStr) {
|
||||
// updateData.startDate = new Date(startDateStr);
|
||||
// }
|
||||
if (!discount) {
|
||||
throw new BadRequestException(DiscountMessage.INVALID_CODE);
|
||||
}
|
||||
|
||||
// if (endDateStr) {
|
||||
// updateData.endDate = new Date(endDateStr);
|
||||
// }
|
||||
// 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);
|
||||
}
|
||||
|
||||
// // Update the discount entity
|
||||
// const updatedDiscount = await this.discountRepository.update(id, updateData);
|
||||
// if (!updatedDiscount) {
|
||||
// throw new NotFoundException(DISCOUNT_ERROR_MESSAGES.NOT_FOUND);
|
||||
// }
|
||||
// Check if the discount has reached its maximum usage
|
||||
if (discount.maxUsage !== null && discount.usedCount >= discount.maxUsage) {
|
||||
throw new BadRequestException(DiscountMessage.MAX_USAGE_REACHED);
|
||||
}
|
||||
|
||||
// // Handle product relations if provided
|
||||
// if (targetProducts !== undefined) {
|
||||
// // Remove existing relations
|
||||
// await this.discountServicesRepository.deleteByDiscountId(id);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// // Add new relations if not empty
|
||||
// if (targetProducts.length > 0) {
|
||||
// await this.discountServicesRepository.createMany(id, targetProducts);
|
||||
// }
|
||||
// }
|
||||
// Check if the discount applies to the items in the invoice
|
||||
if (!this.discountValidator.validateItemsForDiscount(invoice, discount)) {
|
||||
throw new BadRequestException(DiscountMessage.NOT_APPLICABLE);
|
||||
}
|
||||
|
||||
// // Return the updated discount with relations
|
||||
// return this.findById(id);
|
||||
// }
|
||||
return discount;
|
||||
}
|
||||
|
||||
// async delete(id: string): Promise<void> {
|
||||
// const discount = await this.findById(id);
|
||||
// await this.discountRepository.delete(discount.id);
|
||||
// }
|
||||
/**
|
||||
* 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();
|
||||
|
||||
// async toggleActive(id: string, isActive: boolean): Promise<Discount> {
|
||||
// await this.findById(id);
|
||||
// const updatedDiscount = await this.discountRepository.update(id, { isActive });
|
||||
// if (!updatedDiscount) {
|
||||
// throw new NotFoundException(DISCOUNT_ERROR_MESSAGES.NOT_FOUND);
|
||||
// }
|
||||
// return updatedDiscount;
|
||||
// }
|
||||
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"],
|
||||
});
|
||||
|
||||
// async validateDiscount(code: string, user: User, productId?: string): Promise<Discount> {
|
||||
// const discount = await this.discountRepository.findValidByCode(code);
|
||||
if (!invoice) {
|
||||
throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID);
|
||||
}
|
||||
|
||||
// if (!discount) {
|
||||
// throw new BadRequestException(DISCOUNT_ERROR_MESSAGES.INVALID);
|
||||
// }
|
||||
// Check if the invoice already has a discount
|
||||
if (invoice.discount) {
|
||||
throw new BadRequestException(InvoiceMessage.ALREADY_HAS_DISCOUNT);
|
||||
}
|
||||
|
||||
// // Check if user has already used this discount
|
||||
// const usageCount = await this.usageDiscountRepository.countByUserAndDiscount(user.id, discount.id);
|
||||
// if (usageCount > 0) {
|
||||
// throw new BadRequestException(DISCOUNT_ERROR_MESSAGES.ALREADY_USED);
|
||||
// }
|
||||
// Validate the discount code
|
||||
const discount = await this.validateDiscountCode(code, userId, invoice, queryRunner);
|
||||
|
||||
// // Check if product is valid for this discount
|
||||
// if (productId && discount.discountServices && discount.discountServices.length > 0) {
|
||||
// const hasProduct = await this.discountServicesRepository.hasProductForDiscount(discount.id, productId);
|
||||
// if (!hasProduct) {
|
||||
// throw new BadRequestException(DISCOUNT_ERROR_MESSAGES.INVALID_PRODUCT);
|
||||
// }
|
||||
// }
|
||||
// Calculate the discount amount
|
||||
const discountAmount = this.discountValidator.calculateDiscountAmount(invoice, discount);
|
||||
|
||||
// return discount;
|
||||
// }
|
||||
// Calculate the new total price
|
||||
const newTotalPrice = this.discountValidator.calculateDiscountedTotalPrice(invoice, discountAmount);
|
||||
|
||||
// async applyDiscount(amount: number, discountId: string, productId?: string): Promise<number> {
|
||||
// const discount = await this.findById(discountId);
|
||||
// Update the invoice
|
||||
invoice.discount = discount;
|
||||
invoice.totalPrice = new Decimal(newTotalPrice);
|
||||
|
||||
// if (!discount.isValid) {
|
||||
// throw new BadRequestException(DISCOUNT_ERROR_MESSAGES.INVALID);
|
||||
// }
|
||||
await queryRunner.manager.save(Invoice, invoice);
|
||||
|
||||
// // Check if product is valid for this discount
|
||||
// if (productId && discount.discountServices && discount.discountServices.length > 0) {
|
||||
// const hasProduct = await this.discountServicesRepository.hasProductForDiscount(discount.id, productId);
|
||||
// if (!hasProduct) {
|
||||
// throw new BadRequestException(DISCOUNT_ERROR_MESSAGES.INVALID_PRODUCT);
|
||||
// }
|
||||
// }
|
||||
// Record the discount usage
|
||||
const usageDiscount = queryRunner.manager.create(UsageDiscount, {
|
||||
discount,
|
||||
user: { id: userId },
|
||||
appliedAmount: discountAmount,
|
||||
usedAt: new Date(),
|
||||
});
|
||||
|
||||
// let discountAmount = 0;
|
||||
await queryRunner.manager.save(UsageDiscount, usageDiscount);
|
||||
|
||||
// if (discount.type === DiscountType.PERCENTAGE) {
|
||||
// discountAmount = (amount * discount.value) / 100;
|
||||
// } else if (discount.type === DiscountType.FIXED_AMOUNT) {
|
||||
// discountAmount = discount.value;
|
||||
// }
|
||||
// Increment the discount usage count
|
||||
discount.usedCount += 1;
|
||||
await queryRunner.manager.save(Discount, discount);
|
||||
|
||||
// // Ensure discount doesn't exceed the original amount
|
||||
// if (discountAmount > amount) {
|
||||
// discountAmount = amount;
|
||||
// }
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
// return discountAmount;
|
||||
// }
|
||||
return {
|
||||
message: InvoiceMessage.DISCOUNT_APPLIED,
|
||||
invoice,
|
||||
};
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
}
|
||||
|
||||
// async recordDiscountUsage(userId: string, discountId: string, appliedAmount: number): Promise<void> {
|
||||
// const discount = await this.findById(discountId);
|
||||
/**
|
||||
* 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();
|
||||
|
||||
// // Increment usage count
|
||||
// await this.discountRepository.update(discount.id, {
|
||||
// usedCount: discount.usedCount + 1,
|
||||
// });
|
||||
try {
|
||||
// Get the invoice
|
||||
const invoice = await queryRunner.manager.findOne(Invoice, {
|
||||
where: {
|
||||
id: invoiceId,
|
||||
user: { id: userId },
|
||||
status: InvoiceStatus.PENDING,
|
||||
},
|
||||
relations: ["discount", "items"],
|
||||
});
|
||||
|
||||
// // Record usage
|
||||
// await this.usageDiscountRepository.create({
|
||||
// user: { id: userId } as User,
|
||||
// discount: { id: discountId } as Discount,
|
||||
// appliedAmount: new Decimal(appliedAmount),
|
||||
// usedAt: new Date(),
|
||||
// });
|
||||
// }
|
||||
if (!invoice) {
|
||||
throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID);
|
||||
}
|
||||
|
||||
// async getProductsForDiscount(discountId: string): Promise<string[]> {
|
||||
// const discountServices = await this.discountServicesRepository.findByDiscountId(discountId);
|
||||
// return discountServices.map((service) => service.danakService.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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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");
|
||||
return code;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user