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

423 lines
16 KiB
TypeScript

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;
}
}