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, In } from "typeorm"; import { ServiceMessage, SubscriptionMessage, WalletMessage } from "../../../common/enums/message.enum"; import { DanakServicesService } from "../../danak-services/providers/danak-services.service"; import { DiscountCalculationType } from "../../discounts/enums/discount-type.enum"; import { InvoicesService } from "../../invoices/providers/invoices.service"; import { UsersService } from "../../users/providers/users.service"; import { PaginationUtils } from "../../utils/providers/pagination.utils"; import { Wallet } from "../../wallets/entities/wallet.entity"; import { WalletsService } from "../../wallets/providers/wallets.service"; import { AddSubscriptionsToServiceDto } from "../DTO/create-subscription.dto"; import { SearchUserSubsQueryDto } from "../DTO/search-user-subs-query.dto"; import { ServiceSubsQueryDto } from "../DTO/service-subs-query.dto"; import { SubscribeServiceDto } from "../DTO/subscribe-service.dto"; import { UpdateSubscriptionPlanDto } from "../DTO/update-subscription.dto"; import { SubscriptionPlan } from "../entities/subscription.entity"; import { UserSubscription } from "../entities/user-subscription.entity"; import { SubscriptionStatus } from "../enums/subscription-status.enum"; import { SubscriptionsPlanRepository } from "../repositories/subscriptions.repository"; import { UserSubscriptionsRepository } from "../repositories/user-subscriptions.repository"; @Injectable() export class SubscriptionsService { constructor( private readonly subscriptionsPlanRepository: SubscriptionsPlanRepository, private readonly userSubscriptionsRepository: UserSubscriptionsRepository, private readonly invoicesService: InvoicesService, private readonly usersService: UsersService, private readonly walletsService: WalletsService, private readonly danakServices: DanakServicesService, private readonly dataSource: DataSource, ) {} //************************************ */ async createSubscriptionsPlan(createDto: AddSubscriptionsToServiceDto) { const danakService = await this.danakServices.findServiceById(createDto.serviceId); if (!danakService) throw new BadRequestException(ServiceMessage.SERVICE_NOT_FOUND_BY_ID); const subscriptions = createDto.subs.map((sub) => ({ ...sub, service: { id: createDto.serviceId } })); const subscriptionNames = createDto.subs.map((sub) => sub.name); const existingSubscriptions = await this.subscriptionsPlanRepository.find({ where: { name: In(subscriptionNames), service: { id: createDto.serviceId }, }, }); if (existingSubscriptions.length > 0) throw new BadRequestException({ message: [SubscriptionMessage.NAME_EXIST, existingSubscriptions] }); const createdSubscriptions = this.subscriptionsPlanRepository.create(subscriptions); await this.subscriptionsPlanRepository.save(createdSubscriptions); return { message: SubscriptionMessage.CREATED, subscriptions: createdSubscriptions, }; } //************************************ */ async getUserSubscriptions(userId: string, queryDto: SearchUserSubsQueryDto) { const queryBuilder = this.userSubscriptionsRepository .createQueryBuilder("userSubscription") .leftJoinAndSelect("userSubscription.plan", "plan") .leftJoinAndSelect("plan.service", "service") .where("userSubscription.user.id = :userId", { userId }); if (queryDto.q) { queryBuilder.andWhere("(service.name ILIKE :query OR service.title ILIKE :query OR service.description ILIKE :query)", { query: `%${queryDto.q}%`, }); } const subscriptions = await queryBuilder.getMany(); return { subscriptions, }; } //************************************ */ async getUserSubscriptionById(userSubId: string, userId: string) { const userSubscription = await this.userSubscriptionsRepository.findOne({ where: { id: userSubId, user: { id: userId } }, relations: { plan: { service: { images: true, subscriptionPlans: true, }, }, }, }); if (!userSubscription) throw new BadRequestException(SubscriptionMessage.USER_SUBS_NOT_FOUND); return { userSubscription, }; } //************************************ */ async updateSubscriptionPlan(id: string, updateDto: UpdateSubscriptionPlanDto) { if (updateDto.serviceId) { const danakService = await this.danakServices.findServiceById(updateDto.serviceId); if (!danakService) throw new BadRequestException(ServiceMessage.SERVICE_NOT_FOUND_BY_ID); } const subscription = await this.subscriptionsPlanRepository.findOneBy({ id }); if (!subscription) throw new BadRequestException(SubscriptionMessage.NOT_FOUND); if (updateDto.name) { const existSubscription = await this.subscriptionsPlanRepository.findOneByName(updateDto.name, id); if (existSubscription) throw new BadRequestException(SubscriptionMessage.NAME_EXIST); } await this.subscriptionsPlanRepository.update(id, updateDto); await this.subscriptionsPlanRepository.save({ ...subscription, ...updateDto, service: updateDto.serviceId ? { id: updateDto.serviceId } : subscription.service, }); return { message: SubscriptionMessage.UPDATED, subscription, }; } //************************************ */ async getSubscriptionPlanById(subscriptionId: string) { const subscription = await this.subscriptionsPlanRepository.findOneBy({ id: subscriptionId }); if (!subscription) throw new BadRequestException(SubscriptionMessage.NOT_FOUND); return { subscription, }; } //************************************ */ async getServiceSubscriptions(serviceId: string, queryDto: ServiceSubsQueryDto) { const service = await this.danakServices.findServiceById(serviceId); const { limit, skip } = PaginationUtils(queryDto); const queryBuilder = this.subscriptionsPlanRepository .createQueryBuilder("subscription") .leftJoin("subscription.service", "service") .addSelect(["service.id"]) .leftJoin("subscription.discounts", "discount") .addSelect(["discount.calculationType", "discount.amount", "discount.startDate", "discount.endDate", "discount.isActive"]) .where("service.id = :serviceId", { serviceId }); if (queryDto.q) { queryBuilder.andWhere("subscription.name ILIKE :query", { query: `%${queryDto.q}%` }); } if (queryDto.isActive !== undefined) { queryBuilder.andWhere("subscription.isActive = :isActive", { isActive: queryDto.isActive === 1 }); } const [subscriptions, count] = await queryBuilder.skip(skip).take(limit).getManyAndCount(); const transformedSubscriptions = subscriptions.map((subscription) => { let price = new Decimal(subscription.price); if (subscription.discounts && subscription.discounts.length > 0) { subscription.discounts.forEach((discount) => { if (discount.isActive && new Date() >= discount.startDate && new Date() <= discount.endDate) { if (discount.calculationType === DiscountCalculationType.PERCENTAGE) { const discountAmount = price.mul(discount.amount).div(100); price = price.sub(discountAmount); } else if (discount.calculationType === DiscountCalculationType.FIXED) { price = price.sub(discount.amount); } } }); } return { ...subscription, finalPrice: price.toNumber(), }; }); return { service, subscriptions: transformedSubscriptions, count, pagination: true, }; } //************************************ */ async toggleSubStatus(subId: string) { const subscription = await this.subscriptionsPlanRepository.findOneBy({ id: subId }); if (!subscription) throw new BadRequestException(SubscriptionMessage.NOT_FOUND); subscription.isActive = !subscription.isActive; await this.subscriptionsPlanRepository.save(subscription); return { message: SubscriptionMessage.STATUS_UPDATED, isActive: subscription.isActive, }; } //************************************ */ async subscribeToPlan(serviceId: string, subscribeDto: SubscribeServiceDto, userId: string) { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { const user = await this.usersService.findOneByIdWithQueryRunner(userId, queryRunner); const plan = await queryRunner.manager.findOne(SubscriptionPlan, { where: { id: subscribeDto.planId, service: { id: serviceId } }, relations: { service: true, discounts: true, }, }); if (!plan) throw new BadRequestException(SubscriptionMessage.NOT_FOUND); const startDate = new Date(); const endDate = new Date(); endDate.setDate(startDate.getDate() + plan.duration); // const userSubscription = queryRunner.manager.create(UserSubscription, { user, plan, startDate, endDate }); await queryRunner.manager.save(UserSubscription, userSubscription); const userWallet = await this.walletsService.getWalletByUserId(user.id, queryRunner); let finalPrice = new Decimal(plan.price); if (plan.discounts && plan.discounts.length > 0) { plan.discounts.forEach((discount) => { if (discount.isActive && new Date() >= discount.startDate && new Date() <= discount.endDate) { if (discount.calculationType === DiscountCalculationType.PERCENTAGE) { const discountAmount = finalPrice.mul(discount.amount).div(100); finalPrice = finalPrice.sub(discountAmount); } else if (discount.calculationType === DiscountCalculationType.FIXED) { finalPrice = finalPrice.sub(discount.amount); } } }); } plan.price = finalPrice; if (new Decimal(userWallet.balance).lessThan(finalPrice)) throw new BadRequestException(WalletMessage.INSUFFICIENT_BALANCE); // if (userWallet.balance < plan.price) throw new BadRequestException(WalletMessage.INSUFFICIENT_BALANCE); // userWallet.balance = new Decimal(userWallet.balance).sub(finalPrice); await queryRunner.manager.save(Wallet, userWallet); // await this.walletsService.createSubscriptionTransaction(finalPrice, userWallet.id, queryRunner); //TODO: need queue handling for notification const invoiceDueDate = new Date(userSubscription.endDate); invoiceDueDate.setDate(userSubscription.endDate.getDate() - 3); const invoice = await this.invoicesService.createInvoiceForSubscription(userId, plan, invoiceDueDate, queryRunner); userSubscription.status = SubscriptionStatus.ACTIVE; await queryRunner.manager.save(UserSubscription, userSubscription); await queryRunner.commitTransaction(); return { message: SubscriptionMessage.SUBSCRIBED, userSubscription, invoice, }; } catch (error) { await queryRunner.rollbackTransaction(); throw error; } finally { await queryRunner.release(); } } //************************************ */ async getPlanRevenueAverage() { const currentYear = new Date().getFullYear(); const startOfYear = new Date(currentYear, 0, 1); const endOfYear = new Date(currentYear, 11, 31); const result = await this.userSubscriptionsRepository .createQueryBuilder("userSubscription") .leftJoin("userSubscription.plan", "plan") .select("AVG(plan.price)", "averageRevenue") .where("userSubscription.startDate >= :startDate", { startDate: startOfYear }) .andWhere("userSubscription.startDate <= :endDate", { endDate: endOfYear }) .andWhere("userSubscription.status = :status", { status: SubscriptionStatus.ACTIVE }) .getRawOne(); return Number(result.averageRevenue) || 0; } async getPlansSalesCount() { const currentYear = new Date().getFullYear(); const startOfYear = new Date(currentYear, 0, 1); const endOfYear = new Date(currentYear, 11, 31); return await this.userSubscriptionsRepository .createQueryBuilder("userSubscription") .where("userSubscription.startDate >= :startDate", { startDate: startOfYear }) .andWhere("userSubscription.startDate <= :endDate", { endDate: endOfYear }) .andWhere("userSubscription.status = :status", { status: SubscriptionStatus.ACTIVE }) .getCount(); } async getPlansSalesCountByMonthsInYear() { const now = new Date(); const startOfYear = new Date(now.getFullYear(), 0, 1); const endOfYear = new Date(now.getFullYear(), 11, 31); const sales = await this.userSubscriptionsRepository .createQueryBuilder("userSubscription") .select("EXTRACT(MONTH FROM userSubscription.startDate)", "month") .addSelect("COUNT(*)", "count") .where("userSubscription.startDate >= :startDate", { startDate: startOfYear }) .andWhere("userSubscription.startDate <= :endDate", { endDate: endOfYear }) .andWhere("userSubscription.status = :status", { status: SubscriptionStatus.ACTIVE }) .groupBy("EXTRACT(MONTH FROM userSubscription.startDate)") .orderBy("month", "ASC") .getRawMany(); const salesByMonth = Array(12).fill(0); sales.forEach((sale) => { const month = parseInt(sale.month) - 1; salesByMonth[month] = parseInt(sale.count); }); return salesByMonth; } async getPlansSalesCountByDaysInMonth() { const now = new Date(); const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0); const sales = await this.userSubscriptionsRepository .createQueryBuilder("userSubscription") .select("DATE(userSubscription.startDate)", "date") .addSelect("COUNT(*)", "count") .where("userSubscription.startDate >= :startDate", { startDate: startOfMonth }) .andWhere("userSubscription.startDate <= :endDate", { endDate: endOfMonth }) .andWhere("userSubscription.status = :status", { status: SubscriptionStatus.ACTIVE }) .groupBy("DATE(userSubscription.startDate)") .orderBy("date", "ASC") .getRawMany(); const daysInMonth = endOfMonth.getDate(); const salesByDay = Array(daysInMonth).fill(0); sales.forEach((sale) => { const day = new Date(sale.date).getDate() - 1; salesByDay[day] = parseInt(sale.count); }); return salesByDay; } async getPlansSalesCountByDaysInWeek() { const startOfWeek = dayjs().startOf("week").toDate(); const endOfWeek = dayjs().endOf("week").toDate(); console.log(startOfWeek, endOfWeek); const sales = await this.userSubscriptionsRepository .createQueryBuilder("userSubscription") .select("DATE(userSubscription.startDate)", "date") .addSelect("COUNT(*)", "count") .where("userSubscription.startDate >= :startDate", { startDate: startOfWeek }) .andWhere("userSubscription.startDate <= :endDate", { endDate: endOfWeek }) .andWhere("userSubscription.status = :status", { status: SubscriptionStatus.ACTIVE }) .groupBy("DATE(userSubscription.startDate)") .orderBy("date", "ASC") .getRawMany(); const salesByDay = Array(7).fill(0); sales.forEach((sale) => { const day = (dayjs(sale.date).day() + 1) % 7; salesByDay[day] = parseInt(sale.count); }); return salesByDay; } async getSubscriptionGrowthRate() { const currentYear = new Date().getFullYear(); const lastYear = currentYear - 1; const currentYearCount = await this.userSubscriptionsRepository .createQueryBuilder("userSubscription") .where("EXTRACT(YEAR FROM userSubscription.startDate) = :year", { year: currentYear }) .andWhere("userSubscription.status = :status", { status: SubscriptionStatus.ACTIVE }) .getCount(); const lastYearCount = await this.userSubscriptionsRepository .createQueryBuilder("userSubscription") .where("EXTRACT(YEAR FROM userSubscription.startDate) = :year", { year: lastYear }) .andWhere("userSubscription.status = :status", { status: SubscriptionStatus.ACTIVE }) .getCount(); if (lastYearCount === 0) return 100; const growthRate = ((currentYearCount - lastYearCount) / lastYearCount) * 100; return Math.round(growthRate * 100) / 100; } }