chore: add business logic
This commit is contained in:
@@ -478,6 +478,11 @@ export const enum SubscriptionMessage {
|
||||
BUSINESS_DESCRIPTION_STRING = "توضیحات کسب و کار باید یک رشته باشد",
|
||||
IS_ACTIVE_SHOULD_BE_1_0 = "وضعیت اشتراک باید یکی از مقادیر ۰ و ۱ باشد",
|
||||
NOT_PURCHASED_CANNOT_REVIEW = "شما این اشتراک را خریداری نکردهاید و نمیتوانید نظر بدهید",
|
||||
SLUG_REQUIRED = "اسلاگ نمیتواند خالی باشد",
|
||||
SLUG_STRING = "اسلاگ باید یک رشته باشد",
|
||||
SLUG_LENGTH = "اسلاگ باید بین ۴ تا ۲۵۰ کاراکتر باشد",
|
||||
SLUG_EXISTS = "این اسلاگ قبلا ثبت شده است",
|
||||
USER_DOES_NOT_HAVE_ACCESS = "شما دسترسی به این سرویس ندارید",
|
||||
}
|
||||
|
||||
export const enum InvoiceMessage {
|
||||
|
||||
@@ -37,7 +37,7 @@ export class TokensService {
|
||||
const accessExpire = this.configService.getOrThrow<number>("ACCESS_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` });
|
||||
|
||||
|
||||
@@ -46,24 +46,12 @@ export class DashboardService {
|
||||
const invoicesCount = await this.getInvoicesCount();
|
||||
const unreadTickets = await this.unreadTickets();
|
||||
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 {
|
||||
danakServicesCount,
|
||||
customersCount,
|
||||
invoicesCount,
|
||||
unreadTickets,
|
||||
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.addNotifyForWalletDeduction(invoice, user, userWallet, WalletMessage.SUBSCRIPTION_WALLET_TRANSFER);
|
||||
await this.addNotifyAdminForSubscriptionPaymentToQueue(invoice);
|
||||
|
||||
//
|
||||
} else if (invoice.items[0]?.supportPlan) {
|
||||
const oldUserSupportPlan = await queryRunner.manager.findOne(UserSupportPlan, {
|
||||
|
||||
@@ -515,6 +515,7 @@ export class NotificationsService {
|
||||
await this.emailService.sendAdminNotificationEmail({
|
||||
userEmail: data.userEmail,
|
||||
title: NotificationMessage.NEW_SUBSCRIPTION,
|
||||
fullName: data.fullName,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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";
|
||||
|
||||
@@ -25,4 +25,17 @@ export class SubscribeServiceDto {
|
||||
@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." })
|
||||
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 { BaseEntity } from "../../../common/entities/base.entity";
|
||||
@@ -28,6 +28,13 @@ export class UserSubscription extends BaseEntity {
|
||||
@Column({ type: "text", nullable: false })
|
||||
description: string;
|
||||
|
||||
@Column({ type: "varchar", length: 250, unique: true, nullable: true })
|
||||
slug: string;
|
||||
|
||||
@Column({ type: "enum", enum: SubscriptionStatus, default: SubscriptionStatus.INACTIVE })
|
||||
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 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;
|
||||
}
|
||||
//************************************ */
|
||||
}
|
||||
|
||||
@@ -93,4 +93,10 @@ export class SubscriptionsController {
|
||||
removeQuickAccess(@Param() paramDto: ParamDto, @UserDec("id") userId: string) {
|
||||
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 { TypeOrmModule } from "@nestjs/typeorm";
|
||||
|
||||
import { SUBSCRIPTIONS } from "./constants";
|
||||
import { SubscriptionPlan } from "./entities/subscription.entity";
|
||||
import { UserSubscription } from "./entities/user-subscription.entity";
|
||||
import { SubscriptionsService } from "./providers/subscriptions.service";
|
||||
@@ -18,6 +20,10 @@ import { UserQuickAccessRepository } from "./repositories/users-quick-access.rep
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([SubscriptionPlan, UserSubscription, UserQuickAccess]),
|
||||
BullModule.registerQueue({
|
||||
name: SUBSCRIPTIONS.PROVISIONING_QUEUE_NAME,
|
||||
prefix: SUBSCRIPTIONS.PROVISIONING_QUEUE_PREFIX,
|
||||
}),
|
||||
DanakServicesModule,
|
||||
UsersModule,
|
||||
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 = {
|
||||
to: data.userEmail,
|
||||
subject: data.title,
|
||||
@@ -162,6 +162,7 @@ export class EmailService {
|
||||
context: {
|
||||
title: data.title,
|
||||
message: data.message,
|
||||
fullName: data.fullName,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user