chore: add business logic

This commit is contained in:
Mahyargdz
2025-05-18 10:19:16 +03:30
parent 1bc8b23452
commit b5c90e8c2e
12 changed files with 119 additions and 187 deletions
@@ -1,14 +1,19 @@
import { BadRequestException, Injectable } from "@nestjs/common";
import { InjectQueue } from "@nestjs/bullmq";
import { BadRequestException, ForbiddenException, Injectable } from "@nestjs/common";
import { Queue } from "bullmq";
import dayjs from "dayjs";
import Decimal from "decimal.js";
import slugify from "slugify";
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 { User } from "../../users/entities/user.entity";
import { UsersService } from "../../users/providers/users.service";
import { PaginationUtils } from "../../utils/providers/pagination.utils";
import { SUBSCRIPTIONS } from "../constants";
import { AddSubscriptionsToServiceDto } from "../DTO/create-subscription.dto";
import { SearchUserSubsQueryDto } from "../DTO/search-user-subs-query.dto";
import { ServiceSubsQueryDto } from "../DTO/service-subs-query.dto";
@@ -23,6 +28,7 @@ import { UserSubscriptionsRepository } from "../repositories/user-subscriptions.
@Injectable()
export class SubscriptionsService {
constructor(
@InjectQueue(SUBSCRIPTIONS.PROVISIONING_QUEUE_NAME) private readonly provisioningQueue: Queue,
private readonly subscriptionsPlanRepository: SubscriptionsPlanRepository,
private readonly userSubscriptionsRepository: UserSubscriptionsRepository,
private readonly invoicesService: InvoicesService,
@@ -196,7 +202,6 @@ export class SubscriptionsService {
async subscribeToPlan(serviceId: string, subscribeDto: SubscribeServiceDto, userId: string) {
const queryRunner = this.dataSource.createQueryRunner();
try {
await queryRunner.connect();
await queryRunner.startTransaction();
@@ -210,18 +215,35 @@ export class SubscriptionsService {
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 });
let slug = subscribeDto.slug;
if (!slug) {
slug = slugify(subscribeDto.businessName, { lower: true, strict: true });
}
const existingSlug = await queryRunner.manager.findOne(UserSubscription, { where: { slug } });
if (existingSlug) throw new BadRequestException(SubscriptionMessage.SLUG_EXISTS);
let staff: User[] = [];
if (subscribeDto.staff && subscribeDto.staff.length > 0) {
staff = await this.usersService.findUsersByIds(subscribeDto.staff);
}
const userSubscription = queryRunner.manager.create(UserSubscription, {
...subscribeDto,
user,
plan,
startDate,
endDate,
slug,
staff,
});
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);
@@ -229,6 +251,8 @@ export class SubscriptionsService {
await queryRunner.manager.save(UserSubscription, userSubscription);
await this.addProvisioningJob(userSubscription, plan, user);
await queryRunner.commitTransaction();
return {
message: SubscriptionMessage.SUBSCRIBED,
@@ -293,134 +317,43 @@ export class SubscriptionsService {
}
//************************************ */
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);
async getUserSubscriptionsForService(userId: string, serviceId: string) {
const userSubscriptions = await this.userSubscriptionsRepository.find({
where: [
{ user: { id: userId }, plan: { service: { id: serviceId } }, status: SubscriptionStatus.ACTIVE },
{ staff: { id: userId }, plan: { service: { id: serviceId } }, status: SubscriptionStatus.ACTIVE },
],
relations: {
plan: { service: true },
staff: true,
},
});
return salesByMonth;
if (!userSubscriptions.length) throw new ForbiddenException(SubscriptionMessage.USER_DOES_NOT_HAVE_ACCESS);
return { workspaces: userSubscriptions };
}
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 addProvisioningJob(userSubscription: UserSubscription, plan: SubscriptionPlan, user: User) {
await this.provisioningQueue.add(
SUBSCRIPTIONS.PROVISIONING_JOB_NAME,
{
subscriptionId: userSubscription.id,
serviceId: plan.service.id,
serviceName: plan.service.name,
businessName: userSubscription.businessName,
userId: user.id,
slug: userSubscription.slug,
},
{
priority: SUBSCRIPTIONS.PROVISIONING_JOB_PRIORITY,
delay: SUBSCRIPTIONS.PROVISIONING_JOB_DELAY,
attempts: SUBSCRIPTIONS.PROVISIONING_JOB_ATTEMPTS,
backoff: { type: "exponential", delay: SUBSCRIPTIONS.PROVISIONING_JOB_BACKOFF },
},
);
}
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;
}
//************************************ */
}