428 lines
16 KiB
TypeScript
Executable File
428 lines
16 KiB
TypeScript
Executable File
import { BadRequestException, Injectable } from "@nestjs/common";
|
|
import dayjs from "dayjs";
|
|
import Decimal from "decimal.js";
|
|
import { DataSource, In, Not, QueryRunner } from "typeorm";
|
|
|
|
import { ServiceMessage, SubscriptionMessage } from "../../../common/enums/message.enum";
|
|
import { DanakServicesService } from "../../danak-services/providers/danak-services.service";
|
|
import { INVOICE } from "../../invoices/constants";
|
|
import { InvoicesService } from "../../invoices/providers/invoices.service";
|
|
import { UsersService } from "../../users/providers/users.service";
|
|
import { PaginationUtils } from "../../utils/providers/pagination.utils";
|
|
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 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 },
|
|
originalPrice: new Decimal(sub.price),
|
|
}));
|
|
|
|
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) {
|
|
// Find the existing subscription plan
|
|
const subscription = await this.subscriptionsPlanRepository.findOne({ where: { id }, relations: { service: true } });
|
|
if (!subscription) throw new BadRequestException(SubscriptionMessage.NOT_FOUND);
|
|
|
|
// Check if service exists when trying to update it
|
|
if (updateDto.serviceId) {
|
|
const danakService = await this.danakServices.findServiceById(updateDto.serviceId);
|
|
if (!danakService) throw new BadRequestException(ServiceMessage.SERVICE_NOT_FOUND_BY_ID);
|
|
subscription.service = danakService;
|
|
}
|
|
|
|
// Check for name uniqueness if updating name
|
|
if (updateDto.name) {
|
|
const existSubscription = await this.subscriptionsPlanRepository.findOneBy({
|
|
name: updateDto.name,
|
|
id: Not(id),
|
|
service: { id: subscription.service.id },
|
|
});
|
|
if (existSubscription) throw new BadRequestException(SubscriptionMessage.NAME_EXIST);
|
|
subscription.name = updateDto.name;
|
|
}
|
|
|
|
if (updateDto.duration) subscription.duration = updateDto.duration;
|
|
if (updateDto.price) {
|
|
subscription.price = new Decimal(updateDto.price);
|
|
subscription.originalPrice = new Decimal(updateDto.price);
|
|
}
|
|
|
|
await this.subscriptionsPlanRepository.save(subscription);
|
|
|
|
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 All(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();
|
|
|
|
return {
|
|
service,
|
|
subscriptions,
|
|
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,
|
|
directDiscount: true,
|
|
},
|
|
});
|
|
|
|
if (!plan) throw new BadRequestException(SubscriptionMessage.NOT_FOUND);
|
|
|
|
const startDate = dayjs().toDate();
|
|
const endDate = dayjs().add(plan.duration, "day").toDate();
|
|
|
|
//
|
|
const userSubscription = queryRunner.manager.create(UserSubscription, { ...subscribeDto, user, plan, startDate, endDate });
|
|
|
|
await queryRunner.manager.save(UserSubscription, userSubscription);
|
|
//
|
|
|
|
const invoiceDueDate = dayjs(userSubscription.startDate).add(INVOICE.DUEDATE, "day").toDate();
|
|
|
|
const invoice = await this.invoicesService.createInvoiceForSubscription(user, plan, userSubscription, invoiceDueDate, queryRunner);
|
|
userSubscription.status = SubscriptionStatus.INACTIVE;
|
|
|
|
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 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();
|
|
|
|
return {
|
|
service,
|
|
subscriptions,
|
|
count,
|
|
pagination: true,
|
|
};
|
|
}
|
|
//************************************ */
|
|
|
|
async getAllServiceSubscriptions(serviceIds: string[], queryRunner: QueryRunner) {
|
|
const subscriptions = await queryRunner.manager.find(this.subscriptionsPlanRepository.target, {
|
|
where: { service: { id: In(serviceIds) }, isActive: true },
|
|
relations: { directDiscount: true, service: true },
|
|
});
|
|
return subscriptions;
|
|
}
|
|
//************************************ */
|
|
|
|
async countUserSubscriptions(userId: string) {
|
|
const subscriptionCount = await this.userSubscriptionsRepository.count({
|
|
where: { user: { id: userId }, status: SubscriptionStatus.ACTIVE },
|
|
});
|
|
|
|
return subscriptionCount;
|
|
}
|
|
|
|
//************************************ */
|
|
|
|
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();
|
|
|
|
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;
|
|
}
|
|
}
|