diff --git a/src/app.module.ts b/src/app.module.ts index d443f91..9a2ad30 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 0438ac1..b47053a 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -806,7 +806,13 @@ 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 = "شما قبلا پلن پشتیبانی خریداری کردید", + USER_HAS_NO_SUPPORT_PLAN = "کاربر پلن پشتیبانی فعال ندارد", + SUPPORT_PLAN_ALREADY_SUBSCRIBED_OR_INVOICE_EXIST = " کاربر قبلا اشتراک ثبت کرده یا فاکتور آن موجود است و پرداخت نشده است", + USER_ALREADY_HAS_SUPPORT_PLAN = "کاربر قبلا پلن پشتیبانی دارد", + SUPPORT_PLAN_SUBSCRIBED = " اشتراک با موفقیت ثبت شد", + NEW_PLAN_MUST_BE_MORE_EXPENSIVE = "پلن جدید باید گران‌تر از پلن فعلی باشد", + UPGRADE_PRICE_CALCULATION_FAILED = "محاسبه قیمت ارتقا با خطا مواجه شد", + SUPPORT_PLAN_UPGRADE_INITIATED_SUCCESSFULLY = "ارتقا پلن پشتیبانی با موفقیت انجام شد", + SUPPORT_PLAN_ALREADY_SUBSCRIBED = "کاربر قبلا اشتراک ثبت کرده است", + DOWN_GRADE_NOT_ALLOWED = "امکان تغییر به پلن پایین‌تر وجود ندارد", } diff --git a/src/configs/typeorm.config.ts b/src/configs/typeorm.config.ts index 8d8378f..9391ab2 100755 --- a/src/configs/typeorm.config.ts +++ b/src/configs/typeorm.config.ts @@ -13,7 +13,7 @@ export function databaseConfigs(): TypeOrmModuleAsyncOptions { username: configService.getOrThrow("DB_USER"), password: configService.getOrThrow("DB_PASS"), autoLoadEntities: true, - synchronize: configService.getOrThrow("NODE_ENV") == "production" ? false : true, + synchronize: configService.getOrThrow("NODE_ENV") == "production" ? false : false, logging: configService.getOrThrow("NODE_ENV") == "production" ? false : true, migrationsTableName: "typeorm_migrations", migrationsRun: false, diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 890a7ea..65f008d 100755 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -16,6 +16,7 @@ import { SSOTokenValidateDTO } from "./DTO/requests/sso-token-validate.dto"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { UserDec } from "../../common/decorators/user.decorator"; import { User } from "../users/entities/user.entity"; + @ApiTags("Auth") @Controller("auth") @Throttle({ default: { limit: AUTH_THROTTLE_LIMIT, ttl: AUTH_THROTTLE_TTL } }) diff --git a/src/modules/invoices/providers/invoices.service.ts b/src/modules/invoices/providers/invoices.service.ts index 7cba169..3a48e10 100755 --- a/src/modules/invoices/providers/invoices.service.ts +++ b/src/modules/invoices/providers/invoices.service.ts @@ -383,9 +383,16 @@ export class InvoicesService { return invoice; } //********************************** */ - async createInvoiceForSupportPlan(user: User, supPlan: SupportPlan, userSupPlan: UserSupportPlan, dueDate: Date, qryRnr: QueryRunner) { + async createInvoiceForSupportPlan( + user: User, + supPlan: SupportPlan, + userSupPlan: UserSupportPlan, + dueDate: Date, + qryRnr: QueryRunner, + price?: number, + ) { const originalPrice = supPlan.price; - const finalPrice = supPlan.price; + const finalPrice = price || supPlan.price; const invoiceItem = { name: supPlan.name, @@ -593,6 +600,9 @@ export class InvoicesService { subscriptionPlan: { plan: true, }, + supportPlan: { + supportPlan: true, + }, }, }, }); diff --git a/src/modules/support-plans/DTO/create-support-plan.dto.ts b/src/modules/support-plans/DTO/create-support-plan.dto.ts index e5d86c8..3affd9f 100644 --- a/src/modules/support-plans/DTO/create-support-plan.dto.ts +++ b/src/modules/support-plans/DTO/create-support-plan.dto.ts @@ -10,6 +10,7 @@ import { IsString, MaxLength, Min, + ValidateIf, ValidateNested, } from "class-validator"; @@ -30,6 +31,7 @@ export class CreateSupportPlanDto { @ApiProperty({ description: "The description of the support plan" }) description: string; + @ValidateIf((object) => object.price > 0) @IsNotEmpty({ message: SupportPlanMessage.PRICE_REQUIRED }) @IsNumber({}, { message: SupportPlanMessage.PRICE_NUMBER }) @Min(10000, { message: SupportPlanMessage.PRICE_MIN }) diff --git a/src/modules/support-plans/entities/support-plan.entity.ts b/src/modules/support-plans/entities/support-plan.entity.ts index 3d65c51..5fd31c9 100644 --- a/src/modules/support-plans/entities/support-plan.entity.ts +++ b/src/modules/support-plans/entities/support-plan.entity.ts @@ -15,6 +15,9 @@ export class SupportPlan extends BaseEntity { @Column({ type: "decimal", precision: 16, scale: 2, nullable: false, transformer: new DecimalTransformer() }) price: Decimal; + @Column({ type: "boolean", default: false }) + isFree: boolean; + @Column({ type: "int", nullable: false }) duration: number; //days diff --git a/src/modules/support-plans/providers/support-plans.service.ts b/src/modules/support-plans/providers/support-plans.service.ts index 7bdedd2..9d2055f 100644 --- a/src/modules/support-plans/providers/support-plans.service.ts +++ b/src/modules/support-plans/providers/support-plans.service.ts @@ -1,5 +1,6 @@ import { BadRequestException, Injectable } from "@nestjs/common"; import dayjs from "dayjs"; +import { Decimal } from "decimal.js"; import { DataSource } from "typeorm"; import { SupportPlanMessage } from "../../../common/enums/message.enum"; @@ -26,7 +27,7 @@ export class SupportPlansService { async createSupportPlan(createSupportPlanDto: CreateSupportPlanDto) { const supportPlan = this.supportPlanRepo.create(createSupportPlanDto); - await this.supportPlanRepo.save(supportPlan); + await this.supportPlanRepo.save({ ...supportPlan, isFree: createSupportPlanDto.price === 0 }); return { message: SupportPlanMessage.SUPPORT_PLAN_CREATED, supportPlan, @@ -123,13 +124,13 @@ export class SupportPlansService { where: { user: { id: userId }, supportPlan: { id } }, }); - if (existingUserSupportPlan) throw new BadRequestException(SupportPlanMessage.SUPPORT_PLAN_ALREADY_SUBSCRIBED_OR_INVOICE_EXIST); + if (existingUserSupportPlan) throw new BadRequestException(SupportPlanMessage.SUPPORT_PLAN_ALREADY_SUBSCRIBED); const existingUserSupport = await queryRunner.manager.findOne(this.userSupportPlanRepo.target, { where: { user: { id: userId } }, }); - if (existingUserSupport) throw new BadRequestException(SupportPlanMessage.USER_ALREADY_HAS_SUPPORT_PLAN); + if (existingUserSupport) throw new BadRequestException(SupportPlanMessage.SUPPORT_PLAN_ALREADY_SUBSCRIBED_OR_INVOICE_EXIST); const startDate = dayjs().toDate(); const endDate = dayjs().add(supportPlan.duration, "day").toDate(); @@ -163,4 +164,94 @@ export class SupportPlansService { await queryRunner.release(); } } + //***************************************** */ + async getUserSupportPlanOfUser(userId: string) { + const userSupportPlan = await this.userSupportPlanRepo.findOne({ + where: { user: { id: userId }, status: UserSupportPlanStatus.ACTIVE }, + relations: { supportPlan: { features: true } }, + }); + + return { userSupportPlan }; + } + //***************************************** */ + async upgradeSupportPlan(newPlanId: string, userId: string) { + const queryRunner = this.dataSource.createQueryRunner(); + try { + await queryRunner.connect(); + await queryRunner.startTransaction(); + + const user = await this.usersService.findOneByIdWithQueryRunner(userId, queryRunner); + + const currentUserSupportPlan = await queryRunner.manager.findOne(this.userSupportPlanRepo.target, { + where: { user: { id: userId } }, + relations: { supportPlan: true }, + }); + + if (!currentUserSupportPlan) throw new BadRequestException(SupportPlanMessage.USER_HAS_NO_SUPPORT_PLAN); + + const newSupportPlan = await queryRunner.manager.findOne(this.supportPlanRepo.target, { + where: { id: newPlanId }, + }); + + if (!newSupportPlan) throw new BadRequestException(SupportPlanMessage.SUPPORT_PLAN_NOT_FOUND); + + const newSupportPlanPrice = new Decimal(newSupportPlan.price); + + const currentUserSupportPlanPrice = new Decimal(currentUserSupportPlan.supportPlan.price); + // check if new plan is actually an upgrade + if (newSupportPlanPrice.lessThanOrEqualTo(currentUserSupportPlanPrice)) { + throw new BadRequestException(SupportPlanMessage.DOWN_GRADE_NOT_ALLOWED); + } + + // calculate remaining days and price + const remainingDays = dayjs(currentUserSupportPlan.endDate).diff(dayjs(), "day"); + const dailyPrice = currentUserSupportPlanPrice.div(currentUserSupportPlan.supportPlan.duration); + const remainingPrice = dailyPrice.mul(remainingDays); + + // calculate upgrade price + const upgradePrice = newSupportPlanPrice.sub(remainingPrice).round(); + + // create new user support plan + const startDate = dayjs().toDate(); + const endDate = dayjs().add(newSupportPlan.duration, "day").toDate(); + + const newUserSupportPlan = queryRunner.manager.create(this.userSupportPlanRepo.target, { + user, + supportPlan: newSupportPlan, + startDate, + endDate, + status: UserSupportPlanStatus.INACTIVE, + }); + + await queryRunner.manager.save(this.userSupportPlanRepo.target, newUserSupportPlan); + + // create invoice for upgrade + const invoiceDueDate = dayjs(startDate).add(INVOICE.DUEDATE, "day").toDate(); + const invoice = await this.invoicesService.createInvoiceForSupportPlan( + user, + newSupportPlan, + newUserSupportPlan, + invoiceDueDate, + queryRunner, + upgradePrice.toNumber(), + ); + + // cancel old support plan + currentUserSupportPlan.status = UserSupportPlanStatus.CANCELLED; + await queryRunner.manager.save(this.userSupportPlanRepo.target, currentUserSupportPlan); + + await queryRunner.commitTransaction(); + + return { + message: SupportPlanMessage.SUPPORT_PLAN_UPGRADE_INITIATED_SUCCESSFULLY, + invoice, + upgradePrice: upgradePrice.toNumber(), + }; + } 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 b658c7c..0d92fc7 100644 --- a/src/modules/support-plans/repositories/support-plan.repository.ts +++ b/src/modules/support-plans/repositories/support-plan.repository.ts @@ -30,7 +30,7 @@ export class SupportPlanRepository extends Repository { } async getSupportPlansListUser() { - return this.find({ where: { isActive: true, deletedAt: IsNull() }, relations: { features: true } }); + return this.find({ where: { isActive: true, deletedAt: IsNull() }, order: { price: "ASC" }, relations: { features: true } }); } //***************************************** */ async getSupportPlanById(id: string) { diff --git a/src/modules/support-plans/repositories/user-support-plan.repository.ts b/src/modules/support-plans/repositories/user-support-plan.repository.ts index 8521095..b20a7a0 100644 --- a/src/modules/support-plans/repositories/user-support-plan.repository.ts +++ b/src/modules/support-plans/repositories/user-support-plan.repository.ts @@ -3,10 +3,17 @@ import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; import { UserSupportPlan } from "../entities/user-support-plan.entity"; - +import { UserSupportPlanStatus } from "../enums/user-support-plan-status.enum"; @Injectable() export class UserSupportPlanRepository extends Repository { constructor(@InjectRepository(UserSupportPlan) userSupportPlanRepo: Repository) { super(userSupportPlanRepo.target, userSupportPlanRepo.manager, userSupportPlanRepo.queryRunner); } + + async getUserSupportPlanOfUser(userId: string) { + const userSupportPlan = await this.findOne({ + where: { user: { id: userId }, status: UserSupportPlanStatus.ACTIVE }, + }); + return userSupportPlan; + } } diff --git a/src/modules/support-plans/support-plans.controller.ts b/src/modules/support-plans/support-plans.controller.ts index 1345812..105fa64 100644 --- a/src/modules/support-plans/support-plans.controller.ts +++ b/src/modules/support-plans/support-plans.controller.ts @@ -7,8 +7,10 @@ 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 { PermissionsDec } from "../../common/decorators/permission.decorator"; import { UserDec } from "../../common/decorators/user.decorator"; import { ParamDto } from "../../common/DTO/param.dto"; +import { PermissionEnum } from "../users/enums/permission.enum"; @Controller("support-plans") @AuthGuards() @@ -16,7 +18,7 @@ export class SupportPlansController { constructor(private readonly supportPlansService: SupportPlansService) {} @AdminRoute() - // @PermissionsDec(PermissionEnum.SUPPORT_PLAN) + @PermissionsDec(PermissionEnum.SUPPORT_PLAN) @ApiOperation({ summary: "Create a new support plan (admin)" }) @Post() createSupportPlan(@Body() createSupportPlanDto: CreateSupportPlanDto) { @@ -24,7 +26,7 @@ export class SupportPlansController { } @AdminRoute() - // @PermissionsDec(PermissionEnum.SUPPORT_PLAN) + @PermissionsDec(PermissionEnum.SUPPORT_PLAN) @ApiOperation({ summary: "Get all support plans (admin)" }) @Get("list") getSupportPlansList(@Query() queryDto: GetSupportPlanListQueryDto) { @@ -32,7 +34,7 @@ export class SupportPlansController { } @AdminRoute() - // @PermissionsDec(PermissionEnum.SUPPORT_PLAN) + @PermissionsDec(PermissionEnum.SUPPORT_PLAN) @ApiOperation({ summary: "Get a support plan by id (admin)" }) @Get(":id") getSupportPlanById(@Param() paramDto: ParamDto) { @@ -40,7 +42,7 @@ export class SupportPlansController { } @AdminRoute() - // @PermissionsDec(PermissionEnum.SUPPORT_PLAN) + @PermissionsDec(PermissionEnum.SUPPORT_PLAN) @ApiOperation({ summary: "Delete a support plan by id (admin)" }) @Delete(":id") deleteSupportPlanById(@Param() paramDto: ParamDto) { @@ -48,7 +50,7 @@ export class SupportPlansController { } @AdminRoute() - // @PermissionsDec(PermissionEnum.SUPPORT_PLAN) + @PermissionsDec(PermissionEnum.SUPPORT_PLAN) @ApiOperation({ summary: "Update a support plan by id (admin)" }) @Patch(":id") updateSupportPlanById(@Param() paramDto: ParamDto, @Body() updateSupportPlanDto: UpdateSupportPlanDto) { @@ -66,4 +68,16 @@ export class SupportPlansController { subscribeToSupportPlan(@Param() paramDto: ParamDto, @UserDec("id") userId: string) { return this.supportPlansService.subscribeToSupportPlan(paramDto.id, userId); } + + @ApiOperation({ summary: "Get user support plan of user" }) + @Get("user") + getUserSupportPlanOfUser(@UserDec("id") userId: string) { + return this.supportPlansService.getUserSupportPlanOfUser(userId); + } + + @ApiOperation({ summary: "Upgrade user support plan" }) + @Post(":id/upgrade") + upgradeSupportPlan(@Param() paramDto: ParamDto, @UserDec("id") userId: string) { + return this.supportPlansService.upgradeSupportPlan(paramDto.id, userId); + } }