import { BadRequestException, Injectable } from "@nestjs/common"; // eslint-disable-next-line import/no-named-as-default import Decimal from "decimal.js"; import { QueryRunner } from "typeorm"; import { InvoiceMessage } from "../../../common/enums/message.enum"; import { DiscountRepository } from "../../discounts/repositories/discount.repository"; import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity"; import { RoleEnum } from "../../users/enums/role.enum"; import { PaginationUtils } from "../../utils/providers/pagination.utils"; import { ApplyDiscountDto } from "../DTO/apply-discount-invoice.dto"; import { CreateInvoiceDto } from "../DTO/create-invoice.dto"; import { InvoicesSearchQueryDto, UserInvoicesSearchQueryDto } from "../DTO/invoices-search-query.dto"; import { Invoice } from "../entities/invoice.entity"; import { InvoiceStatus } from "../enums/invoice-status.enum"; // import { InvoiceItemsRepository } from "../repositories/invoice-items.repository"; import { InvoicesRepository } from "../repositories/invoices.repository"; @Injectable() export class InvoicesService { constructor( private readonly invoiceRepository: InvoicesRepository, private readonly discountRepository: DiscountRepository, // private readonly invoiceItemsRepository: InvoiceItemsRepository, ) {} ///********************************** */ async createInvoiceAdmin(createDto: CreateInvoiceDto) { // const invoiceItems = createDto.items.map((item) => { return { name: item.name, count: item.count, unitPrice: item.unitPrice, discount: item.discount || 0, totalPrice: item.unitPrice * item.count - (item.unitPrice * item.count * item.discount) / 100, }; }); // const totalPrice = invoiceItems.reduce((sum, item) => new Decimal(item.totalPrice).add(sum), new Decimal(0)); const tax = totalPrice.mul(0.1); // const dueDate = new Date(); dueDate.setDate(dueDate.getDate() + 5); // const invoice = this.invoiceRepository.create({ user: { id: createDto.userId }, totalPrice: totalPrice.toNumber(), items: invoiceItems, tax: tax.toNumber(), dueDate, }); // await this.invoiceRepository.save(invoice); //TODO: notify user return { message: InvoiceMessage.CREATED, invoice, }; } ///********************************** */ async createInvoiceForSubscription(userId: string, plan: SubscriptionPlan, dueDate: Date, queryRunner: QueryRunner) { const invoiceItems = [ { name: plan.service.name, count: 1, unitPrice: plan.price, discount: 0, subscriptionPlan: plan, totalPrice: plan.price }, ]; const invoice = queryRunner.manager.create(Invoice, { totalPrice: plan.price, user: { id: userId }, status: InvoiceStatus.PAID, paidAt: new Date(), dueDate, items: invoiceItems, }); // await queryRunner.manager.save(Invoice, invoice); return invoice; } ///********************************** */ async getInvoices(queryDto: InvoicesSearchQueryDto) { const { limit, skip } = PaginationUtils(queryDto); const queryBuilder = this.invoiceRepository.createQueryBuilder("invoice"); queryBuilder .leftJoinAndSelect("invoice.user", "user") .leftJoinAndSelect("invoice.items", "items") .leftJoinAndSelect("items.subscriptionPlan", "subscriptionPlan", "subscriptionPlan.id IS NOT NULL") .leftJoinAndSelect("subscriptionPlan.service", "service", "subscriptionPlan.id IS NOT NULL"); if (queryDto.q) { queryBuilder .orWhere("user.firstName ILIKE :search", { search: `%${queryDto.q}%` }) .orWhere("user.lastName ILIKE :search", { search: `%${queryDto.q}%` }) .orWhere("user.email ILIKE :search", { search: `%${queryDto.q}%` }); } if (queryDto.status) { queryBuilder.andWhere("invoice.status = :status", { status: queryDto.status }); } if (queryDto.userId) { queryBuilder.andWhere("invoice.user.id = :userId", { userId: queryDto.userId }); } if (queryDto.serviceId) { queryBuilder.andWhere("subscriptionPlan.serviceId = :serviceId", { serviceId: queryDto.serviceId }); } queryBuilder.orderBy("invoice.createdAt", "DESC"); queryBuilder.skip(skip); queryBuilder.take(limit); const [invoices, count] = await queryBuilder.getManyAndCount(); return { invoices, count, paginate: true, }; } //*********************************** */ async getInvoiceById(invoiceId: string, role: RoleEnum, userId: string) { let invoice: Invoice | null; if (role === RoleEnum.ADMIN) { invoice = await this.invoiceRepository.findOne({ where: { id: invoiceId }, relations: { items: { subscriptionPlan: true }, user: true }, }); } else { invoice = await this.invoiceRepository.findOne({ where: { id: invoiceId, user: { id: userId } }, relations: { items: { subscriptionPlan: true } }, }); } if (!invoice) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID); return { invoice, }; } //*********************************** */ async getUserInvoices(queryDto: UserInvoicesSearchQueryDto, userId: string) { const { limit, skip } = PaginationUtils(queryDto); const queryBuilder = this.invoiceRepository.createQueryBuilder("invoice"); queryBuilder .andWhere("invoice.user.id = :userId", { userId: userId }) .leftJoinAndSelect("invoice.items", "items") .leftJoinAndSelect("items.subscriptionPlan", "subscriptionPlan", "subscriptionPlan.id IS NOT NULL") .leftJoinAndSelect("subscriptionPlan.service", "service", "subscriptionPlan.id IS NOT NULL"); if (queryDto.status) { queryBuilder.andWhere("invoice.status = :status", { status: queryDto.status }); } queryBuilder.orderBy("invoice.createdAt", "DESC").skip(skip).take(limit); const [invoices, count] = await queryBuilder.getManyAndCount(); return { invoices, count, paginate: true, }; } //*********************************** */ async applyDiscount(invoiceId: string, applyDiscountDto: ApplyDiscountDto) { const invoice = await this.invoiceRepository.findOneBy({ id: invoiceId }); if (!invoice) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID); const discount = await this.discountRepository.findOne({ where: { code: applyDiscountDto.discountCode, isActive: true }, relations: ["subscriptionPlans", "users"], }); if (!discount) { throw new BadRequestException(`Discount with code ${applyDiscountDto.discountCode} not found or inactive`); } if (discount.subscriptionPlans && discount.subscriptionPlans.length > 0) { const isApplicable = invoice.items.some((item) => { const subscriptionPlan = item.subscriptionPlan; if (!subscriptionPlan) return false; return discount.subscriptionPlans.some((plan) => plan.id === subscriptionPlan.id); }); if (!isApplicable) { throw new BadRequestException("This discount is not applicable to any subscription plan in the invoice."); } } if (discount.users && discount.users.length > 0) { if (!invoice.user || !discount.users.some((user) => user.id === invoice.user.id)) { throw new BadRequestException("This discount is not applicable for this user."); } } invoice.discount = discount; await this.invoiceRepository.save(invoice); return { invoice }; } async calculateFinalPrice(invoiceId: string): Promise { const invoice = await this.invoiceRepository.findOneBy({ id: invoiceId }); if (!invoice) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID); let discountValue = new Decimal(0); if (invoice.discount) { if (invoice.discount.calculationType === "PERCENTAGE") { discountValue = invoice.totalPrice.mul(invoice.discount.amount).div(100); } else if (invoice.discount.calculationType === "FIXED") { discountValue = new Decimal(invoice.discount.amount); } } return invoice.totalPrice.minus(discountValue); } }