From 1ea3195f4f2fc60043b444ca53a4fd8497948a21 Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Mon, 5 May 2025 17:01:13 +0330 Subject: [PATCH] feat: implement support plan subscription functionality --- src/app.module.ts | 6 +- src/common/enums/message.enum.ts | 4 + .../invoices/entities/invoice-item.entity.ts | 5 +- .../invoices/providers/invoices.service.ts | 61 ++++++++++++++++ .../providers/support-plans.service.ts | 73 +++++++++++++++++++ .../repositories/support-plan.repository.ts | 4 + .../support-plans/support-plans.controller.ts | 13 ++++ .../support-plans/support-plans.module.ts | 4 +- .../wallets/providers/wallets.service.ts | 12 +++ 9 files changed, 177 insertions(+), 5 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index 9a2ad30..d443f91 100755 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -7,13 +7,13 @@ import { ConfigModule } from "@nestjs/config"; import { ThrottlerModule } from "@nestjs/throttler"; import { TypeOrmModule } from "@nestjs/typeorm"; import { MailerModule } from "@nestjs-modules/mailer"; -// import { TelegrafModule } from "nestjs-telegraf"; +import { TelegrafModule } from "nestjs-telegraf"; import { bullMqConfig } from "./configs/bullmq.config"; import { cacheConfig } from "./configs/cache.config"; import { mailerConfig } from "./configs/mailer.config"; import { rateLimitConfig } from "./configs/rateLimit.config"; -// import { telegrafConfig } from "./configs/telegraf.config"; +import { telegrafConfig } from "./configs/telegraf.config"; import { databaseConfigs } from "./configs/typeorm.config"; import { HTTPLogger } from "./core/middlewares/logger.middleware"; import { AddressModule } from "./modules/address/address.module"; @@ -43,7 +43,7 @@ import { WalletsModule } from "./modules/wallets/wallets.module"; import { MonitoringModule } from "./monitoring/monitoring.module"; @Module({ imports: [ - // TelegrafModule.forRootAsync(telegrafConfig()), + TelegrafModule.forRootAsync(telegrafConfig()), MailerModule.forRootAsync(mailerConfig()), BullModule.forRootAsync(bullMqConfig()), ThrottlerModule.forRootAsync(rateLimitConfig()), diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 4908c18..0438ac1 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -306,6 +306,7 @@ export const enum WalletMessage { DEPOSIT_WALLET_TRANSFER = "شارژ کیف پول از طریق انتقال بانکی", INSUFFICIENT_BALANCE = "موجودی کیف پول کافی نیست", SUBSCRIPTION_WALLET_TRANSFER = "پرداخت اشتراک از طریق کیف پول", + SUPPORT_PLAN_WALLET_TRANSFER = "پرداخت پلن پشتیبانی از طریق کیف پول", INVOICE_WALLET_TRANSFER = "پرداخت صورت حساب از طریق کیف پول", TRANSACTION_NOT_FOUND = "تراکنش پیدا نشد", REFERRAL_REWARD_WALLET_TRANSFER = "پرداخت پاداش ارجاع از طریق کیف پول", @@ -805,4 +806,7 @@ export const enum SupportPlanMessage { SUPPORT_PLAN_NOT_FOUND = "پلن پشتیبانی مورد نظر یافت نشد", SUPPORT_PLAN_DELETED = "پلن پشتیبانی با موفقیت حذف شد", SUPPORT_PLAN_UPDATED = "پلن پشتیبانی با موفقیت به روز رسانی شد", + SUPPORT_PLAN_SUBSCRIBED = "شما با موفقیت در پلن پشتیبانی عضو شدید", + SUPPORT_PLAN_ALREADY_SUBSCRIBED_OR_INVOICE_EXIST = "شما قبلا در پلن پشتیبانی عضو شدید یا فاکتوری با این پلن وجود دارد", + USER_ALREADY_HAS_SUPPORT_PLAN = "شما قبلا پلن پشتیبانی خریداری کردید", } diff --git a/src/modules/invoices/entities/invoice-item.entity.ts b/src/modules/invoices/entities/invoice-item.entity.ts index 1ee4331..4e292a0 100755 --- a/src/modules/invoices/entities/invoice-item.entity.ts +++ b/src/modules/invoices/entities/invoice-item.entity.ts @@ -5,7 +5,7 @@ import { Invoice } from "./invoice.entity"; import { BaseEntity } from "../../../common/entities/base.entity"; import { DecimalTransformer } from "../../../common/transformers/decimal.transformer"; import { UserSubscription } from "../../subscriptions/entities/user-subscription.entity"; - +import { UserSupportPlan } from "../../support-plans/entities/user-support-plan.entity"; @Entity() export class InvoiceItem extends BaseEntity { @ManyToOne(() => Invoice, (invoice) => invoice.items, { onDelete: "CASCADE" }) @@ -28,4 +28,7 @@ export class InvoiceItem extends BaseEntity { @ManyToOne(() => UserSubscription, { nullable: true, onDelete: "RESTRICT" }) subscriptionPlan: UserSubscription | null; + + @ManyToOne(() => UserSupportPlan, { nullable: true, onDelete: "RESTRICT" }) + supportPlan: UserSupportPlan | null; } diff --git a/src/modules/invoices/providers/invoices.service.ts b/src/modules/invoices/providers/invoices.service.ts index 076b888..7cba169 100755 --- a/src/modules/invoices/providers/invoices.service.ts +++ b/src/modules/invoices/providers/invoices.service.ts @@ -13,6 +13,9 @@ import { NotificationQueue } from "../../notifications/queue/notification.queue" import { SubscriptionPlan } from "../../subscriptions/entities/subscription.entity"; import { UserSubscription } from "../../subscriptions/entities/user-subscription.entity"; import { SubscriptionStatus } from "../../subscriptions/enums/subscription-status.enum"; +import { SupportPlan } from "../../support-plans/entities/support-plan.entity"; +import { UserSupportPlan } from "../../support-plans/entities/user-support-plan.entity"; +import { UserSupportPlanStatus } from "../../support-plans/enums/user-support-plan-status.enum"; import { User } from "../../users/entities/user.entity"; import { UsersService } from "../../users/providers/users.service"; import { OTPService } from "../../utils/providers/otp.service"; @@ -379,6 +382,58 @@ export class InvoicesService { return invoice; } + //********************************** */ + async createInvoiceForSupportPlan(user: User, supPlan: SupportPlan, userSupPlan: UserSupportPlan, dueDate: Date, qryRnr: QueryRunner) { + const originalPrice = supPlan.price; + const finalPrice = supPlan.price; + + const invoiceItem = { + name: supPlan.name, + count: 1, + unitPrice: originalPrice, + discount: 0, + supportPlan: userSupPlan, + totalPrice: finalPrice, + }; + + const taxAmount = new Decimal(finalPrice).mul(0.1); + const totalPrice = new Decimal(finalPrice).add(taxAmount); + + const invoice = qryRnr.manager.create(Invoice, { + user, + totalPrice: totalPrice, + originalPrice: new Decimal(originalPrice).add(new Decimal(originalPrice).mul(0.1)), + tax: taxAmount.toNumber(), + status: InvoiceStatus.WAIT_PAYMENT, + dueDate, + items: [invoiceItem], + }); + + await qryRnr.manager.save(Invoice, invoice); + + await this.notificationQueue.addInvoiceCreationNotification(user.id, { + invoiceId: invoice.numericId.toString(), + dueDate: invoice.dueDate, + createDate: invoice.createdAt, + price: totalPrice.toNumber(), + userPhone: user.phone, + userEmail: user.email, + items: supPlan.name, + }); + + await this.invoiceQueue.add( + INVOICE.REMINDER_JOB_NAME, + { invoiceId: invoice.id }, + { + delay: dayjs(invoice.dueDate).subtract(INVOICE.DAYS_BEFORE_OVERDUE, "day").diff(dayjs()), + attempts: INVOICE.REMINDER_JOB_ATTEMPTS, + backoff: { type: "exponential", delay: INVOICE.REMINDER_JOB_BACKOFF }, + priority: INVOICE.REMINDER_JOB_PRIORITY, + }, + ); + + return invoice; + } ///********************************** */ @@ -486,6 +541,12 @@ export class InvoicesService { await this.walletsService.createSubscriptionTransaction(invoice.totalPrice, userWallet.id, queryRunner); await this.addNotifyAdminForSubscriptionPaymentToQueue(invoice); // + } else if (invoice.items[0]?.supportPlan) { + const userSupportPlan = invoice.items[0].supportPlan; + userSupportPlan.status = UserSupportPlanStatus.ACTIVE; + + await queryRunner.manager.save(UserSupportPlan, userSupportPlan); + await this.walletsService.createSupportPlanTransaction(invoice.totalPrice, userWallet.id, queryRunner); } else { await this.walletsService.createInvoiceTransaction(invoice.totalPrice, userWallet.id, queryRunner); } diff --git a/src/modules/support-plans/providers/support-plans.service.ts b/src/modules/support-plans/providers/support-plans.service.ts index a66def6..7bdedd2 100644 --- a/src/modules/support-plans/providers/support-plans.service.ts +++ b/src/modules/support-plans/providers/support-plans.service.ts @@ -1,17 +1,26 @@ import { BadRequestException, Injectable } from "@nestjs/common"; +import dayjs from "dayjs"; import { DataSource } from "typeorm"; import { SupportPlanMessage } from "../../../common/enums/message.enum"; +import { INVOICE } from "../../invoices/constants"; +import { InvoicesService } from "../../invoices/providers/invoices.service"; +import { UsersService } from "../../users/providers/users.service"; import { CreateSupportPlanDto } from "../DTO/create-support-plan.dto"; import { GetSupportPlanListQueryDto } from "../DTO/get-support-plan-list-query.dto"; import { UpdateSupportPlanDto } from "../DTO/update-support-plan-feature.dto"; +import { UserSupportPlanStatus } from "../enums/user-support-plan-status.enum"; import { SupportPlanFeatureRepository } from "../repositories/support-plan-feature.repository"; import { SupportPlanRepository } from "../repositories/support-plan.repository"; +import { UserSupportPlanRepository } from "../repositories/user-support-plan.repository"; @Injectable() export class SupportPlansService { constructor( private readonly supportPlanRepo: SupportPlanRepository, private readonly supportPlanFeatureRepo: SupportPlanFeatureRepository, + private readonly userSupportPlanRepo: UserSupportPlanRepository, + private readonly usersService: UsersService, + private readonly invoicesService: InvoicesService, private readonly dataSource: DataSource, ) {} @@ -32,6 +41,14 @@ export class SupportPlansService { paginate: true, }; } + + //***************************************** */ + async getSupportPlansListUser() { + const supportPlans = await this.supportPlanRepo.getSupportPlansListUser(); + return { + supportPlans, + }; + } //***************************************** */ async getSupportPlanById(id: string) { const supportPlan = await this.supportPlanRepo.getSupportPlanById(id); @@ -90,4 +107,60 @@ export class SupportPlansService { await queryRunner.release(); } } + //***************************************** */ + async subscribeToSupportPlan(id: string, userId: string) { + const queryRunner = this.dataSource.createQueryRunner(); + try { + await queryRunner.connect(); + await queryRunner.startTransaction(); + + const supportPlan = await queryRunner.manager.findOne(this.supportPlanRepo.target, { where: { id } }); + if (!supportPlan) throw new BadRequestException(SupportPlanMessage.SUPPORT_PLAN_NOT_FOUND); + + const user = await this.usersService.findOneByIdWithQueryRunner(userId, queryRunner); + + const existingUserSupportPlan = await queryRunner.manager.findOne(this.userSupportPlanRepo.target, { + where: { user: { id: userId }, supportPlan: { id } }, + }); + + if (existingUserSupportPlan) throw new BadRequestException(SupportPlanMessage.SUPPORT_PLAN_ALREADY_SUBSCRIBED_OR_INVOICE_EXIST); + + const existingUserSupport = await queryRunner.manager.findOne(this.userSupportPlanRepo.target, { + where: { user: { id: userId } }, + }); + + if (existingUserSupport) throw new BadRequestException(SupportPlanMessage.USER_ALREADY_HAS_SUPPORT_PLAN); + + const startDate = dayjs().toDate(); + const endDate = dayjs().add(supportPlan.duration, "day").toDate(); + + const userSupportPlan = queryRunner.manager.create(this.userSupportPlanRepo.target, { user, supportPlan, startDate, endDate }); + + await queryRunner.manager.save(this.userSupportPlanRepo.target, userSupportPlan); + + const invoiceDueDate = dayjs(userSupportPlan.startDate).add(INVOICE.DUEDATE, "day").toDate(); + + const invoice = await this.invoicesService.createInvoiceForSupportPlan( + user, + supportPlan, + userSupportPlan, + invoiceDueDate, + queryRunner, + ); + userSupportPlan.status = UserSupportPlanStatus.INACTIVE; + + await queryRunner.manager.save(this.userSupportPlanRepo.target, userSupportPlan); + + await queryRunner.commitTransaction(); + return { + message: SupportPlanMessage.SUPPORT_PLAN_SUBSCRIBED, + invoice, + }; + } catch (error) { + await queryRunner.rollbackTransaction(); + throw error; + } finally { + await queryRunner.release(); + } + } } diff --git a/src/modules/support-plans/repositories/support-plan.repository.ts b/src/modules/support-plans/repositories/support-plan.repository.ts index e99e3e5..b658c7c 100644 --- a/src/modules/support-plans/repositories/support-plan.repository.ts +++ b/src/modules/support-plans/repositories/support-plan.repository.ts @@ -28,6 +28,10 @@ export class SupportPlanRepository extends Repository { query.skip(skip).take(limit).orderBy("supportPlan.createdAt", "DESC"); return query.getManyAndCount(); } + + async getSupportPlansListUser() { + return this.find({ where: { isActive: true, deletedAt: IsNull() }, relations: { features: true } }); + } //***************************************** */ async getSupportPlanById(id: string) { return this.findOne({ where: { id, deletedAt: IsNull() }, relations: { features: true } }); diff --git a/src/modules/support-plans/support-plans.controller.ts b/src/modules/support-plans/support-plans.controller.ts index a0ac9c8..1345812 100644 --- a/src/modules/support-plans/support-plans.controller.ts +++ b/src/modules/support-plans/support-plans.controller.ts @@ -7,6 +7,7 @@ import { UpdateSupportPlanDto } from "./DTO/update-support-plan-feature.dto"; import { SupportPlansService } from "./providers/support-plans.service"; import { AdminRoute } from "../../common/decorators/admin.decorator"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; +import { UserDec } from "../../common/decorators/user.decorator"; import { ParamDto } from "../../common/DTO/param.dto"; @Controller("support-plans") @@ -53,4 +54,16 @@ export class SupportPlansController { updateSupportPlanById(@Param() paramDto: ParamDto, @Body() updateSupportPlanDto: UpdateSupportPlanDto) { return this.supportPlansService.updateSupportPlanById(paramDto.id, updateSupportPlanDto); } + + @ApiOperation({ summary: "Get all support plans" }) + @Get() + getSupportPlansListUser() { + return this.supportPlansService.getSupportPlansListUser(); + } + + @ApiOperation({ summary: "subscribe to a support plan" }) + @Post(":id/subscribe") + subscribeToSupportPlan(@Param() paramDto: ParamDto, @UserDec("id") userId: string) { + return this.supportPlansService.subscribeToSupportPlan(paramDto.id, userId); + } } diff --git a/src/modules/support-plans/support-plans.module.ts b/src/modules/support-plans/support-plans.module.ts index fc211f7..827ec67 100644 --- a/src/modules/support-plans/support-plans.module.ts +++ b/src/modules/support-plans/support-plans.module.ts @@ -9,8 +9,10 @@ import { SupportPlanFeatureRepository } from "./repositories/support-plan-featur import { SupportPlanRepository } from "./repositories/support-plan.repository"; import { UserSupportPlanRepository } from "./repositories/user-support-plan.repository"; import { SupportPlansController } from "./support-plans.controller"; +import { InvoicesModule } from "../invoices/invoices.module"; +import { UsersModule } from "../users/users.module"; @Module({ - imports: [TypeOrmModule.forFeature([SupportPlan, SupportPlanFeature, UserSupportPlan])], + imports: [TypeOrmModule.forFeature([SupportPlan, SupportPlanFeature, UserSupportPlan]), UsersModule, InvoicesModule], controllers: [SupportPlansController], providers: [SupportPlansService, SupportPlanRepository, UserSupportPlanRepository, SupportPlanFeatureRepository], exports: [SupportPlansService], diff --git a/src/modules/wallets/providers/wallets.service.ts b/src/modules/wallets/providers/wallets.service.ts index ca85675..12dfde7 100755 --- a/src/modules/wallets/providers/wallets.service.ts +++ b/src/modules/wallets/providers/wallets.service.ts @@ -97,7 +97,19 @@ export class WalletsService { } //*********************************** */ + async createSupportPlanTransaction(amount: Decimal, walletId: string, queryRunner: QueryRunner) { + const transaction = queryRunner.manager.create(WalletTransaction, { + amount, + wallet: { id: walletId }, + type: TransactionType.DEBIT, + description: WalletMessage.SUPPORT_PLAN_WALLET_TRANSFER, + }); + await queryRunner.manager.save(WalletTransaction, transaction); + + return transaction; + } + //*********************************** */ async createInvoiceTransaction(amount: Decimal, walletId: string, queryRunner: QueryRunner) { const transaction = queryRunner.manager.create(WalletTransaction, { amount,