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

436 lines
16 KiB
TypeScript
Executable File

import { InjectQueue } from "@nestjs/bullmq";
import { BadRequestException, ForbiddenException, Injectable, Logger } from "@nestjs/common";
import { Queue } from "bullmq";
import dayjs from "dayjs";
import Decimal from "decimal.js";
import slugify from "slugify";
import { DataSource, In, MoreThan, 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";
import { SubscribeServiceDto } from "../DTO/subscribe-service.dto";
import { SubscriptionAccessDto } from "../DTO/subscription-access-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 { IBusinessProvisioningJob } from "../interfaces/IProvisioning-job-data";
import { SubscriptionsPlanRepository } from "../repositories/subscriptions.repository";
import { UserSubscriptionsRepository } from "../repositories/user-subscriptions.repository";
@Injectable()
export class SubscriptionsService {
private readonly logger: Logger = new Logger(SubscriptionsService.name);
constructor(
@InjectQueue(SUBSCRIPTIONS.PROVISIONING_QUEUE_NAME) private readonly provisioningQueue: Queue,
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 },
price: sub.price < 100_000 ? new Decimal(0) : new Decimal(sub.price),
originalPrice: sub.price < 100_000 ? new Decimal(0) : new Decimal(sub.price),
isFree: sub.price < 100_000,
}));
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) {
const subscription = await this.subscriptionsPlanRepository.findOne({ where: { id }, relations: { service: true } });
if (!subscription) throw new BadRequestException(SubscriptionMessage.NOT_FOUND);
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;
}
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 = updateDto.price < 100_000 ? new Decimal(0) : new Decimal(updateDto.price);
subscription.originalPrice = updateDto.price < 100_000 ? new Decimal(0) : new Decimal(updateDto.price);
}
await this.subscriptionsPlanRepository.save({
...subscription,
isFree: updateDto.price ? updateDto.price < 100_000 : false,
});
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();
let isFree = false;
try {
await queryRunner.connect();
await queryRunner.startTransaction();
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);
if (plan.isFree) {
isFree = true;
const oneMonthAgo = dayjs().subtract(1, "month").toDate();
const existingFreePlanSubscription = await queryRunner.manager.findOne(UserSubscription, {
where: {
user: { id: userId },
plan: { isFree: true },
startDate: MoreThan(oneMonthAgo),
},
relations: {
plan: true,
},
});
if (existingFreePlanSubscription) {
throw new BadRequestException(SubscriptionMessage.FREE_PLAN_LIMIT_EXCEEDED);
}
}
const startDate = dayjs().toDate();
const endDate = dayjs().add(plan.duration, "day").toDate();
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);
userSubscription.status = SubscriptionStatus.INACTIVE;
await queryRunner.manager.save(UserSubscription, userSubscription);
await this.addProvisioningJob(userSubscription, plan, user);
if (isFree) {
await this.invoicesService.payInvoice(invoice.id, user.id, undefined, queryRunner);
}
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 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,
},
order: { endDate: "DESC" },
select: {
id: true,
businessName: true,
businessPhone: true,
description: true,
slug: true,
status: true,
endDate: true,
startDate: true,
plan: { id: true, name: true, duration: true, service: { id: true, name: true, slug: true, title: true } },
staff: { id: true, firstName: true, lastName: true, email: true, phone: true },
},
});
if (!userSubscriptions.length) throw new ForbiddenException(SubscriptionMessage.USER_DOES_NOT_HAVE_ACCESS);
return { workspaces: userSubscriptions };
}
//************************************ */
async checkUserAccessToService(serviceId: string, accessDto: SubscriptionAccessDto) {
const userSubscriptions = await this.userSubscriptionsRepository.findOne({
where: [
{
id: accessDto.danakSubscriptionId,
user: { id: accessDto.userId },
plan: { service: { id: serviceId } },
status: SubscriptionStatus.ACTIVE,
},
{
id: accessDto.danakSubscriptionId,
staff: { id: accessDto.userId },
plan: { service: { id: serviceId } },
status: SubscriptionStatus.ACTIVE,
},
],
relations: {
plan: { service: true },
staff: true,
},
});
return { hasAccess: !!userSubscriptions && userSubscriptions.status === SubscriptionStatus.ACTIVE };
}
//************************************ */
async addProvisioningJob(userSubscription: UserSubscription, plan: SubscriptionPlan, user: User) {
this.logger.debug(`Adding provisioning job for user ${user.id} and subscription ${userSubscription.id}`);
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,
phone: user.phone,
email: user.email,
fullName: `${user.firstName} ${user.lastName}`,
} as IBusinessProvisioningJob,
{
priority: SUBSCRIPTIONS.PROVISIONING_JOB_PRIORITY,
delay: SUBSCRIPTIONS.PROVISIONING_JOB_DELAY,
attempts: SUBSCRIPTIONS.PROVISIONING_JOB_ATTEMPTS,
backoff: { type: "exponential", delay: SUBSCRIPTIONS.PROVISIONING_JOB_BACKOFF },
},
);
}
//************************************ */
}