chore: add business logic
This commit is contained in:
@@ -478,6 +478,11 @@ export const enum SubscriptionMessage {
|
|||||||
BUSINESS_DESCRIPTION_STRING = "توضیحات کسب و کار باید یک رشته باشد",
|
BUSINESS_DESCRIPTION_STRING = "توضیحات کسب و کار باید یک رشته باشد",
|
||||||
IS_ACTIVE_SHOULD_BE_1_0 = "وضعیت اشتراک باید یکی از مقادیر ۰ و ۱ باشد",
|
IS_ACTIVE_SHOULD_BE_1_0 = "وضعیت اشتراک باید یکی از مقادیر ۰ و ۱ باشد",
|
||||||
NOT_PURCHASED_CANNOT_REVIEW = "شما این اشتراک را خریداری نکردهاید و نمیتوانید نظر بدهید",
|
NOT_PURCHASED_CANNOT_REVIEW = "شما این اشتراک را خریداری نکردهاید و نمیتوانید نظر بدهید",
|
||||||
|
SLUG_REQUIRED = "اسلاگ نمیتواند خالی باشد",
|
||||||
|
SLUG_STRING = "اسلاگ باید یک رشته باشد",
|
||||||
|
SLUG_LENGTH = "اسلاگ باید بین ۴ تا ۲۵۰ کاراکتر باشد",
|
||||||
|
SLUG_EXISTS = "این اسلاگ قبلا ثبت شده است",
|
||||||
|
USER_DOES_NOT_HAVE_ACCESS = "شما دسترسی به این سرویس ندارید",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum InvoiceMessage {
|
export const enum InvoiceMessage {
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export class TokensService {
|
|||||||
const accessExpire = this.configService.getOrThrow<number>("ACCESS_TOKEN_EXPIRE");
|
const accessExpire = this.configService.getOrThrow<number>("ACCESS_TOKEN_EXPIRE");
|
||||||
const refreshExpire = this.configService.getOrThrow<number>("REFRESH_TOKEN_EXPIRE");
|
const refreshExpire = this.configService.getOrThrow<number>("REFRESH_TOKEN_EXPIRE");
|
||||||
|
|
||||||
const accessToken = this.jwtService.sign(payload, { expiresIn: `${accessExpire}m` });
|
const accessToken = this.jwtService.sign(payload, { expiresIn: `${accessExpire}h` });
|
||||||
|
|
||||||
const refreshToken = this.jwtService.sign({ id: payload.id }, { expiresIn: `${refreshExpire}d` });
|
const refreshToken = this.jwtService.sign({ id: payload.id }, { expiresIn: `${refreshExpire}d` });
|
||||||
|
|
||||||
|
|||||||
@@ -46,24 +46,12 @@ export class DashboardService {
|
|||||||
const invoicesCount = await this.getInvoicesCount();
|
const invoicesCount = await this.getInvoicesCount();
|
||||||
const unreadTickets = await this.unreadTickets();
|
const unreadTickets = await this.unreadTickets();
|
||||||
const adsCount = await this.getAdsCount();
|
const adsCount = await this.getAdsCount();
|
||||||
const planRevenueAverage = await this.getPlanRevenueAverage();
|
|
||||||
const plansSalesCount = await this.getPlansSalesCount();
|
|
||||||
const subscriptionGrowthRate = await this.getSubscriptionGrowthRate();
|
|
||||||
const plansSalesCountByMonthsInYear = await this.getPlansSalesCountByMonthsInYear();
|
|
||||||
const plansSalesCountByDaysInMonth = await this.getPlansSalesCountByDaysInMonth();
|
|
||||||
const plansSalesCountByDaysInWeek = await this.getPlansSalesCountByDaysInWeek();
|
|
||||||
return {
|
return {
|
||||||
danakServicesCount,
|
danakServicesCount,
|
||||||
customersCount,
|
customersCount,
|
||||||
invoicesCount,
|
invoicesCount,
|
||||||
unreadTickets,
|
unreadTickets,
|
||||||
adsCount,
|
adsCount,
|
||||||
planRevenueAverage,
|
|
||||||
plansSalesCount,
|
|
||||||
subscriptionGrowthRate,
|
|
||||||
plansSalesCountByMonthsInYear,
|
|
||||||
plansSalesCountByDaysInMonth,
|
|
||||||
plansSalesCountByDaysInWeek,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,44 +91,4 @@ export class DashboardService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//************************************ */
|
//************************************ */
|
||||||
|
|
||||||
private async getPlanRevenueAverage() {
|
|
||||||
const planRevenueCount = await this.subscriptionsService.getPlanRevenueAverage();
|
|
||||||
return planRevenueCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
//************************************ */
|
|
||||||
|
|
||||||
private async getPlansSalesCount() {
|
|
||||||
const plansSalesCount = await this.subscriptionsService.getPlansSalesCount();
|
|
||||||
return plansSalesCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
//************************************ */
|
|
||||||
|
|
||||||
private async getSubscriptionGrowthRate() {
|
|
||||||
const subscriptionGrowthRate = await this.subscriptionsService.getSubscriptionGrowthRate();
|
|
||||||
return subscriptionGrowthRate;
|
|
||||||
}
|
|
||||||
|
|
||||||
//************************************ */
|
|
||||||
|
|
||||||
private async getPlansSalesCountByMonthsInYear() {
|
|
||||||
const plansSalesCountByMonthsInYear = await this.subscriptionsService.getPlansSalesCountByMonthsInYear();
|
|
||||||
return plansSalesCountByMonthsInYear;
|
|
||||||
}
|
|
||||||
|
|
||||||
//************************************ */
|
|
||||||
|
|
||||||
private async getPlansSalesCountByDaysInMonth() {
|
|
||||||
const plansSalesCountByDaysInMonth = await this.subscriptionsService.getPlansSalesCountByDaysInMonth();
|
|
||||||
return plansSalesCountByDaysInMonth;
|
|
||||||
}
|
|
||||||
|
|
||||||
//************************************ */
|
|
||||||
|
|
||||||
private async getPlansSalesCountByDaysInWeek() {
|
|
||||||
const plansSalesCountByDaysInWeek = await this.subscriptionsService.getPlansSalesCountByDaysInWeek();
|
|
||||||
return plansSalesCountByDaysInWeek;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -578,6 +578,7 @@ export class InvoicesService {
|
|||||||
await this.walletsService.createSubscriptionTransaction(invoice.totalPrice, userWallet.id, queryRunner);
|
await this.walletsService.createSubscriptionTransaction(invoice.totalPrice, userWallet.id, queryRunner);
|
||||||
await this.addNotifyForWalletDeduction(invoice, user, userWallet, WalletMessage.SUBSCRIPTION_WALLET_TRANSFER);
|
await this.addNotifyForWalletDeduction(invoice, user, userWallet, WalletMessage.SUBSCRIPTION_WALLET_TRANSFER);
|
||||||
await this.addNotifyAdminForSubscriptionPaymentToQueue(invoice);
|
await this.addNotifyAdminForSubscriptionPaymentToQueue(invoice);
|
||||||
|
|
||||||
//
|
//
|
||||||
} else if (invoice.items[0]?.supportPlan) {
|
} else if (invoice.items[0]?.supportPlan) {
|
||||||
const oldUserSupportPlan = await queryRunner.manager.findOne(UserSupportPlan, {
|
const oldUserSupportPlan = await queryRunner.manager.findOne(UserSupportPlan, {
|
||||||
|
|||||||
@@ -515,6 +515,7 @@ export class NotificationsService {
|
|||||||
await this.emailService.sendAdminNotificationEmail({
|
await this.emailService.sendAdminNotificationEmail({
|
||||||
userEmail: data.userEmail,
|
userEmail: data.userEmail,
|
||||||
title: NotificationMessage.NEW_SUBSCRIPTION,
|
title: NotificationMessage.NEW_SUBSCRIPTION,
|
||||||
|
fullName: data.fullName,
|
||||||
message,
|
message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { IsNotEmpty, IsPhoneNumber, IsString, IsUUID, Length } from "class-validator";
|
import { IsArray, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, IsUUID, Length } from "class-validator";
|
||||||
|
|
||||||
import { SubscriptionMessage } from "../../../common/enums/message.enum";
|
import { SubscriptionMessage } from "../../../common/enums/message.enum";
|
||||||
|
|
||||||
@@ -25,4 +25,17 @@ export class SubscribeServiceDto {
|
|||||||
@Length(5, 500, { message: SubscriptionMessage.BUSINESS_DESCRIPTION_LENGTH })
|
@Length(5, 500, { message: SubscriptionMessage.BUSINESS_DESCRIPTION_LENGTH })
|
||||||
@ApiProperty({ description: "A short description of the business or service", example: "We offer the best subscription plans." })
|
@ApiProperty({ description: "A short description of the business or service", example: "We offer the best subscription plans." })
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNotEmpty({ message: SubscriptionMessage.SLUG_REQUIRED })
|
||||||
|
@IsString({ message: SubscriptionMessage.SLUG_STRING })
|
||||||
|
@Length(4, 250, { message: SubscriptionMessage.SLUG_LENGTH })
|
||||||
|
@ApiProperty({ description: "Unique slug for public URLs", example: "my-restaurant", required: false })
|
||||||
|
slug?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsUUID(4, { each: true })
|
||||||
|
@ApiProperty({ description: "Array of user IDs to add as staff", type: [String], required: false })
|
||||||
|
staff?: string[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export const SUBSCRIPTIONS = Object.freeze({
|
||||||
|
PROVISIONING_QUEUE_PREFIX: "provisioning",
|
||||||
|
PROVISIONING_QUEUE_NAME: "provisioning",
|
||||||
|
PROVISIONING_JOB_NAME: "business.created",
|
||||||
|
|
||||||
|
PROVISIONING_JOB_DELAY: 1000,
|
||||||
|
PROVISIONING_JOB_PRIORITY: 1, // high priority
|
||||||
|
PROVISIONING_JOB_ATTEMPTS: 3, // retry 3 times
|
||||||
|
PROVISIONING_JOB_BACKOFF: 5 * 1000, // retry after 5 second
|
||||||
|
PROVISIONING_JOB_TIMEOUT: 10000, //
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Column, Entity, ManyToOne } from "typeorm";
|
import { Column, Entity, JoinTable, ManyToMany, ManyToOne } from "typeorm";
|
||||||
|
|
||||||
import { SubscriptionPlan } from "./subscription.entity";
|
import { SubscriptionPlan } from "./subscription.entity";
|
||||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||||
@@ -28,6 +28,13 @@ export class UserSubscription extends BaseEntity {
|
|||||||
@Column({ type: "text", nullable: false })
|
@Column({ type: "text", nullable: false })
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
|
@Column({ type: "varchar", length: 250, unique: true, nullable: true })
|
||||||
|
slug: string;
|
||||||
|
|
||||||
@Column({ type: "enum", enum: SubscriptionStatus, default: SubscriptionStatus.INACTIVE })
|
@Column({ type: "enum", enum: SubscriptionStatus, default: SubscriptionStatus.INACTIVE })
|
||||||
status: SubscriptionStatus;
|
status: SubscriptionStatus;
|
||||||
|
|
||||||
|
@ManyToMany(() => User)
|
||||||
|
@JoinTable()
|
||||||
|
staff: User[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 dayjs from "dayjs";
|
||||||
import Decimal from "decimal.js";
|
import Decimal from "decimal.js";
|
||||||
|
import slugify from "slugify";
|
||||||
import { DataSource, In, Not, QueryRunner } from "typeorm";
|
import { DataSource, In, Not, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
import { ServiceMessage, SubscriptionMessage } from "../../../common/enums/message.enum";
|
import { ServiceMessage, SubscriptionMessage } from "../../../common/enums/message.enum";
|
||||||
import { DanakServicesService } from "../../danak-services/providers/danak-services.service";
|
import { DanakServicesService } from "../../danak-services/providers/danak-services.service";
|
||||||
import { INVOICE } from "../../invoices/constants";
|
import { INVOICE } from "../../invoices/constants";
|
||||||
import { InvoicesService } from "../../invoices/providers/invoices.service";
|
import { InvoicesService } from "../../invoices/providers/invoices.service";
|
||||||
|
import { User } from "../../users/entities/user.entity";
|
||||||
import { UsersService } from "../../users/providers/users.service";
|
import { UsersService } from "../../users/providers/users.service";
|
||||||
import { PaginationUtils } from "../../utils/providers/pagination.utils";
|
import { PaginationUtils } from "../../utils/providers/pagination.utils";
|
||||||
|
import { SUBSCRIPTIONS } from "../constants";
|
||||||
import { AddSubscriptionsToServiceDto } from "../DTO/create-subscription.dto";
|
import { AddSubscriptionsToServiceDto } from "../DTO/create-subscription.dto";
|
||||||
import { SearchUserSubsQueryDto } from "../DTO/search-user-subs-query.dto";
|
import { SearchUserSubsQueryDto } from "../DTO/search-user-subs-query.dto";
|
||||||
import { ServiceSubsQueryDto } from "../DTO/service-subs-query.dto";
|
import { ServiceSubsQueryDto } from "../DTO/service-subs-query.dto";
|
||||||
@@ -23,6 +28,7 @@ import { UserSubscriptionsRepository } from "../repositories/user-subscriptions.
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class SubscriptionsService {
|
export class SubscriptionsService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@InjectQueue(SUBSCRIPTIONS.PROVISIONING_QUEUE_NAME) private readonly provisioningQueue: Queue,
|
||||||
private readonly subscriptionsPlanRepository: SubscriptionsPlanRepository,
|
private readonly subscriptionsPlanRepository: SubscriptionsPlanRepository,
|
||||||
private readonly userSubscriptionsRepository: UserSubscriptionsRepository,
|
private readonly userSubscriptionsRepository: UserSubscriptionsRepository,
|
||||||
private readonly invoicesService: InvoicesService,
|
private readonly invoicesService: InvoicesService,
|
||||||
@@ -196,7 +202,6 @@ export class SubscriptionsService {
|
|||||||
|
|
||||||
async subscribeToPlan(serviceId: string, subscribeDto: SubscribeServiceDto, userId: string) {
|
async subscribeToPlan(serviceId: string, subscribeDto: SubscribeServiceDto, userId: string) {
|
||||||
const queryRunner = this.dataSource.createQueryRunner();
|
const queryRunner = this.dataSource.createQueryRunner();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await queryRunner.connect();
|
await queryRunner.connect();
|
||||||
await queryRunner.startTransaction();
|
await queryRunner.startTransaction();
|
||||||
@@ -210,18 +215,35 @@ export class SubscriptionsService {
|
|||||||
directDiscount: true,
|
directDiscount: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!plan) throw new BadRequestException(SubscriptionMessage.NOT_FOUND);
|
if (!plan) throw new BadRequestException(SubscriptionMessage.NOT_FOUND);
|
||||||
|
|
||||||
const startDate = dayjs().toDate();
|
const startDate = dayjs().toDate();
|
||||||
const endDate = dayjs().add(plan.duration, "day").toDate();
|
const endDate = dayjs().add(plan.duration, "day").toDate();
|
||||||
|
|
||||||
//
|
let slug = subscribeDto.slug;
|
||||||
const userSubscription = queryRunner.manager.create(UserSubscription, { ...subscribeDto, user, plan, startDate, endDate });
|
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);
|
await queryRunner.manager.save(UserSubscription, userSubscription);
|
||||||
//
|
|
||||||
|
|
||||||
const invoiceDueDate = dayjs(userSubscription.startDate).add(INVOICE.DUEDATE, "day").toDate();
|
const invoiceDueDate = dayjs(userSubscription.startDate).add(INVOICE.DUEDATE, "day").toDate();
|
||||||
|
|
||||||
const invoice = await this.invoicesService.createInvoiceForSubscription(user, plan, userSubscription, invoiceDueDate, queryRunner);
|
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 queryRunner.manager.save(UserSubscription, userSubscription);
|
||||||
|
|
||||||
|
await this.addProvisioningJob(userSubscription, plan, user);
|
||||||
|
|
||||||
await queryRunner.commitTransaction();
|
await queryRunner.commitTransaction();
|
||||||
return {
|
return {
|
||||||
message: SubscriptionMessage.SUBSCRIBED,
|
message: SubscriptionMessage.SUBSCRIBED,
|
||||||
@@ -293,134 +317,43 @@ export class SubscriptionsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//************************************ */
|
//************************************ */
|
||||||
|
async getUserSubscriptionsForService(userId: string, serviceId: string) {
|
||||||
async getPlanRevenueAverage() {
|
const userSubscriptions = await this.userSubscriptionsRepository.find({
|
||||||
const currentYear = new Date().getFullYear();
|
where: [
|
||||||
const startOfYear = new Date(currentYear, 0, 1);
|
{ user: { id: userId }, plan: { service: { id: serviceId } }, status: SubscriptionStatus.ACTIVE },
|
||||||
const endOfYear = new Date(currentYear, 11, 31);
|
{ staff: { id: userId }, plan: { service: { id: serviceId } }, status: SubscriptionStatus.ACTIVE },
|
||||||
|
],
|
||||||
const result = await this.userSubscriptionsRepository
|
relations: {
|
||||||
.createQueryBuilder("userSubscription")
|
plan: { service: true },
|
||||||
.leftJoin("userSubscription.plan", "plan")
|
staff: true,
|
||||||
.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;
|
if (!userSubscriptions.length) throw new ForbiddenException(SubscriptionMessage.USER_DOES_NOT_HAVE_ACCESS);
|
||||||
|
|
||||||
|
return { workspaces: userSubscriptions };
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPlansSalesCountByDaysInMonth() {
|
//************************************ */
|
||||||
const now = new Date();
|
async addProvisioningJob(userSubscription: UserSubscription, plan: SubscriptionPlan, user: User) {
|
||||||
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
await this.provisioningQueue.add(
|
||||||
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
SUBSCRIPTIONS.PROVISIONING_JOB_NAME,
|
||||||
|
{
|
||||||
const sales = await this.userSubscriptionsRepository
|
subscriptionId: userSubscription.id,
|
||||||
.createQueryBuilder("userSubscription")
|
serviceId: plan.service.id,
|
||||||
.select("DATE(userSubscription.startDate)", "date")
|
serviceName: plan.service.name,
|
||||||
.addSelect("COUNT(*)", "count")
|
businessName: userSubscription.businessName,
|
||||||
.where("userSubscription.startDate >= :startDate", { startDate: startOfMonth })
|
userId: user.id,
|
||||||
.andWhere("userSubscription.startDate <= :endDate", { endDate: endOfMonth })
|
slug: userSubscription.slug,
|
||||||
.andWhere("userSubscription.status = :status", { status: SubscriptionStatus.ACTIVE })
|
},
|
||||||
.groupBy("DATE(userSubscription.startDate)")
|
{
|
||||||
.orderBy("date", "ASC")
|
priority: SUBSCRIPTIONS.PROVISIONING_JOB_PRIORITY,
|
||||||
.getRawMany();
|
delay: SUBSCRIPTIONS.PROVISIONING_JOB_DELAY,
|
||||||
|
attempts: SUBSCRIPTIONS.PROVISIONING_JOB_ATTEMPTS,
|
||||||
const daysInMonth = endOfMonth.getDate();
|
backoff: { type: "exponential", delay: SUBSCRIPTIONS.PROVISIONING_JOB_BACKOFF },
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,4 +93,10 @@ export class SubscriptionsController {
|
|||||||
removeQuickAccess(@Param() paramDto: ParamDto, @UserDec("id") userId: string) {
|
removeQuickAccess(@Param() paramDto: ParamDto, @UserDec("id") userId: string) {
|
||||||
return this.userQuickAccessService.removeQuickAccess(userId, paramDto.id);
|
return this.userQuickAccessService.removeQuickAccess(userId, paramDto.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "Check if user has access to a service and return all their workspaces for that service (internal use)" })
|
||||||
|
@Get("workspaces/:serviceId")
|
||||||
|
async getUserServiceAccess(@Param() serviceIdParamDto: ServiceIdParamDto, @UserDec("id") userId: string) {
|
||||||
|
return this.subscriptionService.getUserSubscriptionsForService(userId, serviceIdParamDto.serviceId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import { BullModule } from "@nestjs/bullmq";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||||
|
|
||||||
|
import { SUBSCRIPTIONS } from "./constants";
|
||||||
import { SubscriptionPlan } from "./entities/subscription.entity";
|
import { SubscriptionPlan } from "./entities/subscription.entity";
|
||||||
import { UserSubscription } from "./entities/user-subscription.entity";
|
import { UserSubscription } from "./entities/user-subscription.entity";
|
||||||
import { SubscriptionsService } from "./providers/subscriptions.service";
|
import { SubscriptionsService } from "./providers/subscriptions.service";
|
||||||
@@ -18,6 +20,10 @@ import { UserQuickAccessRepository } from "./repositories/users-quick-access.rep
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([SubscriptionPlan, UserSubscription, UserQuickAccess]),
|
TypeOrmModule.forFeature([SubscriptionPlan, UserSubscription, UserQuickAccess]),
|
||||||
|
BullModule.registerQueue({
|
||||||
|
name: SUBSCRIPTIONS.PROVISIONING_QUEUE_NAME,
|
||||||
|
prefix: SUBSCRIPTIONS.PROVISIONING_QUEUE_PREFIX,
|
||||||
|
}),
|
||||||
DanakServicesModule,
|
DanakServicesModule,
|
||||||
UsersModule,
|
UsersModule,
|
||||||
WalletsModule,
|
WalletsModule,
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export class EmailService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendAdminNotificationEmail(data: { userEmail: string; title: string; message: string }) {
|
async sendAdminNotificationEmail(data: { userEmail: string; title: string; message: string; fullName?: string }) {
|
||||||
const emailData = {
|
const emailData = {
|
||||||
to: data.userEmail,
|
to: data.userEmail,
|
||||||
subject: data.title,
|
subject: data.title,
|
||||||
@@ -162,6 +162,7 @@ export class EmailService {
|
|||||||
context: {
|
context: {
|
||||||
title: data.title,
|
title: data.title,
|
||||||
message: data.message,
|
message: data.message,
|
||||||
|
fullName: data.fullName,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user