From 45036227da1f067533b2978633d7209a1ce47e1f Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Wed, 12 Nov 2025 17:37:31 +0330 Subject: [PATCH] add : extend user subscription --- .../DTO/subscribe-service.dto.ts | 13 +++ .../DTO/user-subscription-id.param.dto.ts | 11 +++ .../providers/subscriptions.service.ts | 82 ++++++++++++++++++- .../subscriptions/subscriptions.controller.ts | 15 +++- 4 files changed, 118 insertions(+), 3 deletions(-) create mode 100755 src/modules/subscriptions/DTO/user-subscription-id.param.dto.ts diff --git a/src/modules/subscriptions/DTO/subscribe-service.dto.ts b/src/modules/subscriptions/DTO/subscribe-service.dto.ts index 5af1651..2f46d12 100755 --- a/src/modules/subscriptions/DTO/subscribe-service.dto.ts +++ b/src/modules/subscriptions/DTO/subscribe-service.dto.ts @@ -41,3 +41,16 @@ export class SubscribeServiceDto { @ApiProperty({ description: "Array of user IDs to add as staff", type: [String], required: false }) staff?: string[]; } + +export class ExtendUSerSubscribeServiceDto { + @IsNotEmpty({ message: SubscriptionMessage.PLAN_ID_REQUIRED }) + @IsUUID(4, { message: SubscriptionMessage.PLAN_ID_SHOULD_BE_UUID }) + @ApiProperty({ description: "The plan id to subscribe to", example: "f7b3b3b3-7b3b-4b3b-8b3b-3b3b3b3b3b3b" }) + planId: string; + + @IsNotEmpty({ message: SubscriptionMessage.SERVICE_MUST_BE_UUID }) + @IsUUID(4, { message: SubscriptionMessage.SERVICE_MUST_BE_UUID }) + @ApiProperty({ description: "The service id ", example: "f7b3b3b3-7b3b-4b3b-8b3b-3b3b3b3b3b3b" }) + serviceId: string; + +} diff --git a/src/modules/subscriptions/DTO/user-subscription-id.param.dto.ts b/src/modules/subscriptions/DTO/user-subscription-id.param.dto.ts new file mode 100755 index 0000000..1b265ec --- /dev/null +++ b/src/modules/subscriptions/DTO/user-subscription-id.param.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsNotEmpty, IsUUID } from "class-validator"; + +import { CommonMessage } from "../../../common/enums/message.enum"; + +export class UserSubscriptionIdParamDto { + @IsNotEmpty({ message: CommonMessage.ID_REQUIRED }) + @IsUUID("4", { message: CommonMessage.ID_SHOULD_BE_UUID }) + @ApiProperty({ description: "Id of user Subscription Id", example: "8b1e8b1e-8b1e-8b1e-8b1e-8b1e8b1e8b1e" }) + userSubscriptionId: string; +} diff --git a/src/modules/subscriptions/providers/subscriptions.service.ts b/src/modules/subscriptions/providers/subscriptions.service.ts index 57b75e0..59b1940 100755 --- a/src/modules/subscriptions/providers/subscriptions.service.ts +++ b/src/modules/subscriptions/providers/subscriptions.service.ts @@ -17,7 +17,7 @@ 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 { ExtendUSerSubscribeServiceDto, 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"; @@ -300,6 +300,86 @@ export class SubscriptionsService { await queryRunner.release(); } } + //************************************ */ + + async extendSubscribeToPlan(userSubscriptionId: string, dto: ExtendUSerSubscribeServiceDto, userId: string) { + const queryRunner = this.dataSource.createQueryRunner(); + + let isFree = false; + try { + await queryRunner.connect(); + await queryRunner.startTransaction(); + + const userSubscription = await queryRunner.manager.findOne(UserSubscription, { + where: { id: userSubscriptionId }, + relations: { + user: true, + plan: true, + }, + }); + if (!userSubscription) throw new BadRequestException(SubscriptionMessage.NOT_FOUND); + + const user = await this.usersService.findOneByIdWithQueryRunner(userId, queryRunner); + + const plan = await queryRunner.manager.findOne(SubscriptionPlan, { + where: { id: dto.planId, service: { id: dto.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 = + userSubscription && dayjs(userSubscription.endDate).isAfter(dayjs()) ? dayjs(userSubscription.endDate).toDate() : dayjs().toDate(); + + const endDate = dayjs(startDate).add(plan.duration, "day").toDate(); + + // await queryRunner.manager.save(UserSubscription, userSubscription); + const invoiceDueDate = dayjs().add(INVOICE.DUEDATE, "day").toDate(); + + const invoice = await this.invoicesService.createInvoiceForSubscription(user, plan, userSubscription, invoiceDueDate, queryRunner); + + userSubscription.plan = plan; + userSubscription.endDate = endDate; + await queryRunner.manager.save(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); diff --git a/src/modules/subscriptions/subscriptions.controller.ts b/src/modules/subscriptions/subscriptions.controller.ts index a584282..d0fd054 100755 --- a/src/modules/subscriptions/subscriptions.controller.ts +++ b/src/modules/subscriptions/subscriptions.controller.ts @@ -1,13 +1,14 @@ -import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from "@nestjs/common"; +import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from "@nestjs/common"; import { ApiOperation } from "@nestjs/swagger"; import { AddSubscriptionsToServiceDto } from "./DTO/create-subscription.dto"; import { SearchUserSubsQueryDto } from "./DTO/search-user-subs-query.dto"; import { ServiceIdParamDto } from "./DTO/service-id.param.dto"; import { ServiceSubsQueryDto } from "./DTO/service-subs-query.dto"; -import { SubscribeServiceDto } from "./DTO/subscribe-service.dto"; +import { ExtendUSerSubscribeServiceDto, SubscribeServiceDto } from "./DTO/subscribe-service.dto"; import { SubscriptionAccessDto } from "./DTO/subscription-access-dto"; import { UpdateSubscriptionPlanDto } from "./DTO/update-subscription.dto"; +import { UserSubscriptionIdParamDto } from "./DTO/user-subscription-id.param.dto"; import { SubscriptionsService } from "./providers/subscriptions.service"; import { UserQuickAccessService } from "./providers/users-quick-access.service"; import { ApiKeyGuards } from "../../common/decorators/api-key-guard.decorator"; @@ -79,6 +80,16 @@ export class SubscriptionsController { return this.subscriptionService.subscribeToPlan(paramDto.serviceId, subscribeDto, userId); } + @ApiOperation({ summary: "Extend subscribtion to a service ==> user route" }) + @Put(":userSubscriptionId/subscribe") + extendSubscribe( + @Param() paramDto: UserSubscriptionIdParamDto, + @Body() subscribeDto: ExtendUSerSubscribeServiceDto, + @UserDec("id") userId: string, + ) { + return this.subscriptionService.extendSubscribeToPlan(paramDto.userSubscriptionId, subscribeDto, userId); + } + @ApiOperation({ summary: "add quick access a service ==> user route" }) @Post(":id/add-quick-access") addQuickAccess(@Param() paramDto: ParamDto, @UserDec("id") userId: string) {