From 99cb569ced57868db529ec5f5f51bd57c1422512 Mon Sep 17 00:00:00 2001 From: "aakash.reddy@techdome.net.in" Date: Thu, 9 Oct 2025 14:59:11 +0530 Subject: [PATCH 01/20] feat: downgrade setup for hub --- .../billing/controllers/stripe.controller.ts | 18 +++++-- .../billing/payloads/stripe.payload.ts | 28 ++++++++++ .../stripe-subscription.repository.ts | 53 +++++++++++++++++-- src/modules/common/models/billing.model.ts | 14 +++++ src/modules/common/models/team.model.ts | 4 +- 5 files changed, 108 insertions(+), 9 deletions(-) diff --git a/src/modules/billing/controllers/stripe.controller.ts b/src/modules/billing/controllers/stripe.controller.ts index ce9eaaa7d..6772c93b3 100644 --- a/src/modules/billing/controllers/stripe.controller.ts +++ b/src/modules/billing/controllers/stripe.controller.ts @@ -49,6 +49,7 @@ import { SalesEmailRepository } from "@src/modules/workspace/repositories/sales- import { ConfigService } from "@nestjs/config"; import { TrialType } from "@src/modules/common/enum/trial.enum"; import { PricingPlan } from "@src/modules/common/models/pricing.model"; +import { StripeSubscriptionRepository } from "../repositories/stripe-subscription.repository"; // Dynamically import Stripe services let StripeService: any; @@ -72,6 +73,7 @@ export class StripeController { private readonly pricingService: PricingService, private readonly configService: ConfigService, private readonly salesEmailRepository: SalesEmailRepository, + private readonly stripeSubscriptionRepository: StripeSubscriptionRepository, ) { this.isStripeAvailable = !!this.stripeService; @@ -295,10 +297,10 @@ export class StripeController { for (const planBilling of currentPlan.billing) { if (planBilling.providers?.stripe === createSubscriptionDto.priceId) { selectedPlan = currentPlan; - break; + break; } } - if (selectedPlan) break; + if (selectedPlan) break; } if (selectedPlan) { // Replace or set planName @@ -458,13 +460,21 @@ export class StripeController { @Body() cancelSubscriptionDto: CancelSubscriptionDto, ): Promise { try { + console.log( + "-----------this is the subscription-data ---->", + cancelSubscriptionDto, + subscriptionId, + ); this.checkStripeAvailability(); - const subscription = await this.stripeService.cancelSubscription( subscriptionId, false, //disables cancellation at mid cycle ); - + await this.stripeSubscriptionRepository.addDowngradeDetails( + cancelSubscriptionDto.teamId, + cancelSubscriptionDto.workspaceIds, + cancelSubscriptionDto.userIds, + ); return { subscription }; } catch (error) { throw new HttpException( diff --git a/src/modules/billing/payloads/stripe.payload.ts b/src/modules/billing/payloads/stripe.payload.ts index 8bac5b331..ac6a6f08f 100644 --- a/src/modules/billing/payloads/stripe.payload.ts +++ b/src/modules/billing/payloads/stripe.payload.ts @@ -7,6 +7,8 @@ import { IsBoolean, IsNotEmpty, IsNumber, + IsArray, + ValidateNested, } from "class-validator"; export class CreateCustomerDto { @@ -210,6 +212,32 @@ export class CancelSubscriptionDto { @IsOptional() @IsBoolean() cancelImmediately?: boolean; + + @ApiPropertyOptional({ + description: "team Id", + example: "688731d965594536e3ffdce1", + }) + @IsOptional() + @IsString() + teamId?: string; + + @ApiPropertyOptional({ + description: "List of workspace IDs to downgrade", + example: ["6889ff80b1338511b5bfc210"], + }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + workspaceIds?: string[]; + + @ApiPropertyOptional({ + description: "List of user IDs to downgrade", + example: ["example@gmail.com"], + }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + userIds?: string[]; } export class ReactivateSubscriptionDto { diff --git a/src/modules/billing/repositories/stripe-subscription.repository.ts b/src/modules/billing/repositories/stripe-subscription.repository.ts index ec60d728d..fea25e0e1 100644 --- a/src/modules/billing/repositories/stripe-subscription.repository.ts +++ b/src/modules/billing/repositories/stripe-subscription.repository.ts @@ -110,11 +110,11 @@ export class StripeSubscriptionRepository { return await this.db .collection(Collections.TEAM) .findOne({ - $or: [ - { "billing.customerId": customerId }, + $or: [ + { "billing.customerId": customerId }, { "billing.paymentProviders.customerId": customerId } ] - }); + }); } catch (error) { throw error; } @@ -248,4 +248,51 @@ export class StripeSubscriptionRepository { throw error; } } + + /** + * Add downgrade details (workspaces and users) to a team's billing record. + * This function updates the billing section of a team document by appending + * workspace IDs to `billing.downgrade_workspaces` and user IDs to + * `billing.downgrade_users`. Duplicate entries are automatically avoided + * using MongoDB's `$addToSet` operator. + * @param teamId The unique identifier of the team whose billing record will be updated. + * @param workspaceIds Array of workspace IDs to mark for downgrade. + * @param userIds Array of user IDs to mark for downgrade. + * @returns MongoDB UpdateResult indicating the success or failure of the update. + */ + async addDowngradeDetails( + teamId: string, + workspaceIds: string[], + userIds: string[], + ): Promise { + try { + if (!teamId) { + throw new Error("teamId is required to update downgrade details."); + } + const updateQuery: Record = {}; + // Add workspaces if provided + if (workspaceIds && workspaceIds.length > 0) { + updateQuery["billing.downgrade_workspaces"] = { $each: workspaceIds }; + } + // Add users if provided + if (userIds && userIds.length > 0) { + updateQuery["billing.downgrade_users"] = { $each: userIds }; + } + if (Object.keys(updateQuery).length === 0) { + throw new Error("No workspaceIds or userIds provided to update."); + } + const teamObjectId = new ObjectId(teamId); + const result = await this.db.collection(Collections.TEAM).updateOne( + { _id: teamObjectId }, + { + $addToSet: updateQuery, + $set: { "billing.updatedBy": "system" }, + }, + ); + return result; + } catch (error) { + console.error("Error adding downgrade workspaces and users:", error); + throw error; + } + } } diff --git a/src/modules/common/models/billing.model.ts b/src/modules/common/models/billing.model.ts index 7e0a439c2..6ee6b0f8b 100644 --- a/src/modules/common/models/billing.model.ts +++ b/src/modules/common/models/billing.model.ts @@ -179,6 +179,20 @@ export class BillingDto { subscription_expired_email_sent?: boolean | Date; } +export class BilingDtoWithDowngradeDetails extends BillingDto{ + @IsArray() + @ValidateNested({ each: true }) + @Type(() => String) + @IsOptional() + downgrade_workspaces?:string[]; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => String) + @IsOptional() + downgrade_users?:string[]; +} + class BillingChangeDto { @IsString() @IsOptional() diff --git a/src/modules/common/models/team.model.ts b/src/modules/common/models/team.model.ts index 53020643b..cf51936ff 100644 --- a/src/modules/common/models/team.model.ts +++ b/src/modules/common/models/team.model.ts @@ -19,7 +19,7 @@ import { UserDto } from "./user.model"; import { ObjectId } from "mongodb"; import { SelectedWorkspaces } from "@src/modules/identity/payloads/teamUser.payload"; import { Plan } from "./plan.model"; -import { BillingDto } from "./billing.model"; +import { BilingDtoWithDowngradeDetails, BillingDto } from "./billing.model"; import { LicensesDto } from "./licenses.model"; export class logoDto { @@ -120,7 +120,7 @@ export class Team { @IsOptional() @IsObject() - billing?: BillingDto; + billing?: BilingDtoWithDowngradeDetails; @IsOptional() @IsObject() From 0fe9a68a29655765de820bd61a6380f7fb59abd9 Mon Sep 17 00:00:00 2001 From: "aakash.reddy@techdome.net.in" Date: Sat, 11 Oct 2025 23:00:29 +0530 Subject: [PATCH 02/20] feat: setup of downgrade and autograde for hub --- src/modules/billing/billing.module.ts | 12 ++ .../billing/controllers/stripe.controller.ts | 30 ++-- .../payloads/downgrade-user.payload.ts | 114 +++++++++++++++ .../billing/payloads/stripe.payload.ts | 14 ++ .../repositories/downgradeTeam.reposiotry.ts | 33 +++++ .../repositories/downgradeUser.repository.ts | 41 ++++++ .../downgradeWorkspace.repository.ts | 31 ++++ .../stripe-subscription.repository.ts | 108 ++++++++++++-- .../billing/services/downgrade.service.ts | 136 ++++++++++++++++++ .../services/stripe-subscription.service.ts | 126 +++++++++++++--- src/modules/common/models/billing.model.ts | 12 +- src/modules/common/models/team.model.ts | 17 ++- .../identity/guards/hub-invite.guard.ts | 7 +- .../identity/guards/user-limt-guard.ts | 5 +- .../create-testflow-block-guard.ts | 8 +- .../plan-limits/create-testflow-guard.ts | 3 + .../create-testflow-schedule-guard.ts | 3 + .../plan-limits/create-workspace-guard.ts | 3 + .../plan-limits/workspace-invite.guard.ts | 3 + 19 files changed, 647 insertions(+), 59 deletions(-) create mode 100644 src/modules/billing/payloads/downgrade-user.payload.ts create mode 100644 src/modules/billing/repositories/downgradeTeam.reposiotry.ts create mode 100644 src/modules/billing/repositories/downgradeUser.repository.ts create mode 100644 src/modules/billing/repositories/downgradeWorkspace.repository.ts create mode 100644 src/modules/billing/services/downgrade.service.ts diff --git a/src/modules/billing/billing.module.ts b/src/modules/billing/billing.module.ts index b59f8113f..e7559ca4b 100644 --- a/src/modules/billing/billing.module.ts +++ b/src/modules/billing/billing.module.ts @@ -22,6 +22,10 @@ import { UserRepository } from "@src/modules/identity/repositories/user.reposito import { PricingService } from "@src/modules/workspace/services/pricing.repository"; import { PricingRepository } from "@src/modules/workspace/repositories/pricing.repository"; import { SalesEmailRepository } from "../workspace/repositories/sales-email.repository"; +import { DownGradeTeamRepository } from "./repositories/downgradeTeam.reposiotry"; +import { DownGradeUserRepository } from "./repositories/downgradeUser.repository"; +import { DownGradeWorkspaceRepository } from "./repositories/downgradeWorkspace.repository"; +import { DownGradeService } from "./services/downgrade.service"; // Try to import the Stripe module, but don't crash if it's not available let StripeModule: any; @@ -48,6 +52,10 @@ export class BillingModule { BillingAuditRepository, PromoCodeRepository, PricingRepository, + DownGradeTeamRepository, + DownGradeUserRepository, + DownGradeWorkspaceRepository, + DownGradeService, BillingAuditService, PromoCodeService, PricingService, @@ -73,6 +81,10 @@ export class BillingModule { BillingAuditRepository, BillingAuditService, PromoCodeService, + DownGradeTeamRepository, + DownGradeUserRepository, + DownGradeWorkspaceRepository, + DownGradeService, PricingService, PaymentEmailService, PaymentEmailHelper, diff --git a/src/modules/billing/controllers/stripe.controller.ts b/src/modules/billing/controllers/stripe.controller.ts index 6772c93b3..6c2b33e2b 100644 --- a/src/modules/billing/controllers/stripe.controller.ts +++ b/src/modules/billing/controllers/stripe.controller.ts @@ -425,6 +425,13 @@ export class StripeController { updateSubscriptionDto.seats, updateSubscriptionDto.paymentBehavior, ); + if (subscription) { + await this.stripeSubscriptionRepository.addDowngradeDetails( + updateSubscriptionDto?.metadata?.hubId, + updateSubscriptionDto?.workspaceIds, + updateSubscriptionDto?.userIds, + ); + } return subscription; } catch (error) { @@ -460,21 +467,18 @@ export class StripeController { @Body() cancelSubscriptionDto: CancelSubscriptionDto, ): Promise { try { - console.log( - "-----------this is the subscription-data ---->", - cancelSubscriptionDto, - subscriptionId, - ); this.checkStripeAvailability(); const subscription = await this.stripeService.cancelSubscription( subscriptionId, false, //disables cancellation at mid cycle ); - await this.stripeSubscriptionRepository.addDowngradeDetails( - cancelSubscriptionDto.teamId, - cancelSubscriptionDto.workspaceIds, - cancelSubscriptionDto.userIds, - ); + if (subscription) { + await this.stripeSubscriptionRepository.addDowngradeDetails( + cancelSubscriptionDto.teamId, + cancelSubscriptionDto.workspaceIds, + cancelSubscriptionDto.userIds, + ); + } return { subscription }; } catch (error) { throw new HttpException( @@ -519,7 +523,11 @@ export class StripeController { subscriptionId, reactivateDto.metadata, ); - + if (subscription) { + await this.stripeSubscriptionRepository.removeDowngradeDetails( + reactivateDto.metadata?.hubId, + ); + } return { subscription }; } catch (error) { throw new HttpException( diff --git a/src/modules/billing/payloads/downgrade-user.payload.ts b/src/modules/billing/payloads/downgrade-user.payload.ts new file mode 100644 index 000000000..b59cf5c4c --- /dev/null +++ b/src/modules/billing/payloads/downgrade-user.payload.ts @@ -0,0 +1,114 @@ +import { IsNotEmpty, IsMongoId } from "class-validator"; +import { ApiProperty } from "@nestjs/swagger"; +import { + IsString, + IsOptional, + IsEmail, + IsArray, + ValidateNested, + IsBoolean, + IsDate, +} from "class-validator"; +import { Type } from "class-transformer"; +import { ObjectId } from "mongodb"; + +export class DowngradeUserDto { + @ApiProperty({ example: "64f03af32e420f7f68055b92" }) + @IsMongoId() + @IsNotEmpty() + teamId: string; + + @ApiProperty({ example: "64f03af32e420f7f68055b92" }) + @IsMongoId() + @IsNotEmpty() + userId: string; +} + +export class TourGuideDto { + @IsBoolean() + @IsOptional() + isRequestTestsNoCodeDemoCompleted?: boolean; + + @IsBoolean() + @IsOptional() + isGenerateVariableDemoCompleted?: boolean; + + @IsBoolean() + @IsOptional() + isRequestTestsScriptDemoCompleted?: boolean; +} + +class UserWorkspaceDto { + @IsMongoId() + @IsNotEmpty() + workspaceId: string; + + @IsString() + @IsNotEmpty() + name: string; + + @IsMongoId() + @IsNotEmpty() + teamId: string; + + @IsBoolean() + @IsOptional() + isNewInvite?: boolean; +} + +class TeamDto { + @IsMongoId() + @IsNotEmpty() + id: ObjectId; + + @IsString() + @IsNotEmpty() + name: string; + + @IsString() + @IsNotEmpty() + role: string; + + @IsBoolean() + @IsOptional() + isNewInvite?: boolean; + + @IsOptional() + @IsDate() + joinedAt?: Date; +} + +class DownGradeUserDto { + @IsString() + @IsOptional() + name?: string; + + @IsEmail() + @IsOptional() + email?: string; + + @IsArray() + @IsOptional() + @Type(() => TeamDto) + teams?: TeamDto[]; + + @IsArray() + @Type(() => UserWorkspaceDto) + @IsOptional() + @ValidateNested({ each: true }) + workspaces?: UserWorkspaceDto[]; +} + +class DownGradeUserGenerateVariableDto extends DownGradeUserDto { + @IsArray() + @IsString({ each: true }) + @Type(() => String) + isGenerateVariableTrial?: string[]; +} + +export class DownGradeUserTourGuideDto extends DownGradeUserGenerateVariableDto { + @IsOptional() + @ValidateNested() + @Type(() => TourGuideDto) + tourGuide?: TourGuideDto; +} diff --git a/src/modules/billing/payloads/stripe.payload.ts b/src/modules/billing/payloads/stripe.payload.ts index ac6a6f08f..98ae9c027 100644 --- a/src/modules/billing/payloads/stripe.payload.ts +++ b/src/modules/billing/payloads/stripe.payload.ts @@ -201,6 +201,20 @@ export class UpdateSubscriptionDto { @IsNumber() seats?: number; paymentBehavior?: "default_incomplete" | "allow_incomplete"; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + workspaceIds?: string[]; + + @ApiPropertyOptional({ + description: "List of user IDs to downgrade", + example: ["example@gmail.com"], + }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + userIds?: string[]; } export class CancelSubscriptionDto { diff --git a/src/modules/billing/repositories/downgradeTeam.reposiotry.ts b/src/modules/billing/repositories/downgradeTeam.reposiotry.ts new file mode 100644 index 000000000..ab784aeea --- /dev/null +++ b/src/modules/billing/repositories/downgradeTeam.reposiotry.ts @@ -0,0 +1,33 @@ +import { Injectable, Inject } from "@nestjs/common"; +import { Collections } from "@src/modules/common/enum/database.collection.enum"; +import { Team, TeamDto } from "@src/modules/common/models/team.model"; +import { Db } from "mongodb"; +import { ObjectId, WithId } from "mongodb"; + +/** + * Repository for handling promo code database operations + */ +@Injectable() +export class DownGradeTeamRepository { + constructor(@Inject("DATABASE_CONNECTION") private db: Db) {} + + async findTeamByTeamId(id: ObjectId): Promise> { + const teamData = await this.db + .collection(Collections.TEAM) + .findOne({ _id: id }); + return teamData; + } + + async updateTeamById( + id: ObjectId, + updateParams: Partial, + ): Promise> { + const updatedTeamParams = { + $set: updateParams, + }; + const responseData = await this.db + .collection(Collections.TEAM) + .findOneAndUpdate({ _id: id }, updatedTeamParams); + return responseData.value; + } +} diff --git a/src/modules/billing/repositories/downgradeUser.repository.ts b/src/modules/billing/repositories/downgradeUser.repository.ts new file mode 100644 index 000000000..fb640ffb5 --- /dev/null +++ b/src/modules/billing/repositories/downgradeUser.repository.ts @@ -0,0 +1,41 @@ +import { Injectable, Inject } from "@nestjs/common"; +import { Db } from "mongodb"; +import { ObjectId, WithId } from "mongodb"; +import { User } from "@src/modules/common/models/user.model"; +import { Collections } from "@src/modules/common/enum/database.collection.enum"; +import { DownGradeUserTourGuideDto } from "../payloads/downgrade-user.payload"; + +/** + * Repository for handling promo code database operations + */ +@Injectable() +export class DownGradeUserRepository { + constructor(@Inject("DATABASE_CONNECTION") private db: Db) {} + async findUserByUserId(id: ObjectId): Promise> { + const userData = await this.db + .collection(Collections.USER) + .findOne({ _id: id }); + return userData; + } + + async updateUserById( + id: ObjectId, + updateParams: Partial, + ): Promise> { + const updatedUserParams = { + $set: updateParams, + }; + const responseData = await this.db + .collection(Collections.USER) + .findOneAndUpdate({ _id: id }, updatedUserParams); + return responseData.value; + } + + async findUsersByIdArray(IdArray: Array): Promise[]> { + const response = await this.db + .collection(Collections.USER) + .find({ _id: { $in: IdArray } }) + .toArray(); + return response; + } +} diff --git a/src/modules/billing/repositories/downgradeWorkspace.repository.ts b/src/modules/billing/repositories/downgradeWorkspace.repository.ts new file mode 100644 index 000000000..2850ef668 --- /dev/null +++ b/src/modules/billing/repositories/downgradeWorkspace.repository.ts @@ -0,0 +1,31 @@ +import { Injectable, Inject, BadRequestException } from "@nestjs/common"; +import { Collections } from "@src/modules/common/enum/database.collection.enum"; +import { Workspace } from "@src/modules/common/models/workspace.model"; +import { Db, DeleteResult } from "mongodb"; +import { ObjectId, WithId } from "mongodb"; + +/** + * Repository for handling promo code database operations + */ +@Injectable() +export class DownGradeWorkspaceRepository { + constructor(@Inject("DATABASE_CONNECTION") private db: Db) {} + + async get(id: string): Promise> { + const _id = new ObjectId(id); + const data = await this.db + .collection(Collections.WORKSPACE) + .findOne({ _id }); + if (!data) { + throw new BadRequestException("Not Found"); + } + return data; + } + + delete(id: string): Promise { + const _id = new ObjectId(id); + return this.db + .collection(Collections.WORKSPACE) + .deleteOne({ _id }); + } +} diff --git a/src/modules/billing/repositories/stripe-subscription.repository.ts b/src/modules/billing/repositories/stripe-subscription.repository.ts index fea25e0e1..da6079f33 100644 --- a/src/modules/billing/repositories/stripe-subscription.repository.ts +++ b/src/modules/billing/repositories/stripe-subscription.repository.ts @@ -107,13 +107,11 @@ export class StripeSubscriptionRepository { */ async findTeamByCustomerId(customerId: string): Promise { try { - return await this.db - .collection(Collections.TEAM) - .findOne({ + return await this.db.collection(Collections.TEAM).findOne({ $or: [ { "billing.customerId": customerId }, - { "billing.paymentProviders.customerId": customerId } - ] + { "billing.paymentProviders.customerId": customerId }, + ], }); } catch (error) { throw error; @@ -252,8 +250,8 @@ export class StripeSubscriptionRepository { /** * Add downgrade details (workspaces and users) to a team's billing record. * This function updates the billing section of a team document by appending - * workspace IDs to `billing.downgrade_workspaces` and user IDs to - * `billing.downgrade_users`. Duplicate entries are automatically avoided + * workspace IDs to `billing.downgrade.workspaces` and user IDs to + * `billing.downgrade.users`. Duplicate entries are automatically avoided * using MongoDB's `$addToSet` operator. * @param teamId The unique identifier of the team whose billing record will be updated. * @param workspaceIds Array of workspace IDs to mark for downgrade. @@ -272,11 +270,11 @@ export class StripeSubscriptionRepository { const updateQuery: Record = {}; // Add workspaces if provided if (workspaceIds && workspaceIds.length > 0) { - updateQuery["billing.downgrade_workspaces"] = { $each: workspaceIds }; + updateQuery["downgrade.workspaceIds"] = { $each: workspaceIds }; } // Add users if provided if (userIds && userIds.length > 0) { - updateQuery["billing.downgrade_users"] = { $each: userIds }; + updateQuery["downgrade.userIds"] = { $each: userIds }; } if (Object.keys(updateQuery).length === 0) { throw new Error("No workspaceIds or userIds provided to update."); @@ -286,12 +284,100 @@ export class StripeSubscriptionRepository { { _id: teamObjectId }, { $addToSet: updateQuery, - $set: { "billing.updatedBy": "system" }, + $set: { + updatedBy: "system", + manual_downgrade: true, + updatedAt: new Date(), + }, + }, + ); + return result; + } catch (error) { + console.error("Error adding downgrade details:", error); + throw error; + } + } + + async removeDowngradeDetails(teamId: string): Promise { + try { + if (!teamId) { + throw new Error("teamId is required to remove downgrade details."); + } + const teamObjectId = new ObjectId(teamId); + const result = await this.db.collection(Collections.TEAM).updateOne( + { _id: teamObjectId }, + { + $unset: { + downgrade: "", // Removes the entire downgrade object + }, + $set: { + manual_downgrade: false, + updatedBy: "system", + updatedAt: new Date(), + }, + }, + ); + return result; + } catch (error) { + console.error("Error removing downgrade details:", error); + throw error; + } + } + + /** + * Enable automated downgrade for a team + * Sets auto_downgrade flag and optionally updates plan active state + * @param teamId - The team ID to update + * @param planActive - Optional boolean to set plan.active state + * @returns UpdateResult from MongoDB + */ + async enableAutoDowngrade(teamId: string): Promise { + try { + if (!teamId) { + throw new Error("teamId is required to enable auto downgrade."); + } + const setQuery: Record = { + auto_downgrade: true, + updatedBy: "system", + updatedAt: new Date(), + }; + setQuery["plan.active"] = false; + const teamObjectId = new ObjectId(teamId); + const result = await this.db + .collection(Collections.TEAM) + .updateOne({ _id: teamObjectId }, { $set: setQuery }); + return result; + } catch (error) { + console.error("Error enabling auto downgrade:", error); + throw error; + } + } + + /** + * Disable automated downgrade for a team + * Clears auto_downgrade flag + * @param teamId - The team ID to update + * @returns UpdateResult from MongoDB + */ + async disableAutoDowngrade(teamId: string): Promise { + try { + if (!teamId) { + throw new Error("teamId is required to disable auto downgrade."); + } + const teamObjectId = new ObjectId(teamId); + const result = await this.db.collection(Collections.TEAM).updateOne( + { _id: teamObjectId }, + { + $set: { + auto_downgrade: false, + updatedBy: "system", + updatedAt: new Date(), + }, }, ); return result; } catch (error) { - console.error("Error adding downgrade workspaces and users:", error); + console.error("Error disabling auto downgrade:", error); throw error; } } diff --git a/src/modules/billing/services/downgrade.service.ts b/src/modules/billing/services/downgrade.service.ts new file mode 100644 index 000000000..e5f86c784 --- /dev/null +++ b/src/modules/billing/services/downgrade.service.ts @@ -0,0 +1,136 @@ +import { Injectable } from "@nestjs/common"; +import { TeamRole } from "@src/modules/common/enum/roles.enum"; +import { Team } from "@src/modules/common/models/team.model"; +import { DeleteResult, ObjectId, WithId } from "mongodb"; +import { DowngradeUserDto } from "../payloads/downgrade-user.payload"; +import { DownGradeTeamRepository } from "../repositories/downgradeTeam.reposiotry"; +import { DownGradeUserRepository } from "../repositories/downgradeUser.repository"; +import { LicenseManagementService } from "./license-management.service"; +import { DownGradeWorkspaceRepository } from "../repositories/downgradeWorkspace.repository"; +import { isString } from "class-validator"; + +@Injectable() +export class DownGradeService { + constructor( + private readonly downgradeTeamRepository: DownGradeTeamRepository, + private readonly downgradeUserRepository: DownGradeUserRepository, + private readonly licenseManagementService: LicenseManagementService, + private readonly downgradeWorkspaceReposiory: DownGradeWorkspaceRepository, + ) {} + + async removeUserFromTeam(payload: DowngradeUserDto): Promise> { + const teamFilter = new ObjectId(payload.teamId); + const teamData = + await this.downgradeTeamRepository.findTeamByTeamId(teamFilter); + const userFilter = new ObjectId(payload.userId); + const userData = + await this.downgradeUserRepository.findUserByUserId(userFilter); + const teamAdmins = [...teamData.admins]; + let userTeamRole; + for (const item of userData.teams) { + if (item.id.toString() === payload.teamId) { + userTeamRole = item.role; + } + } + const teamUser = [...teamData.users]; + let filteredAdmin; + const filteredData = teamUser.filter( + (item) => item.id.toString() !== payload.userId.toString(), + ); + if (userTeamRole === TeamRole.ADMIN) { + filteredAdmin = teamAdmins.filter( + (id: string) => id.toString() !== payload.userId.toString(), + ); + } + const teamUpdatedParams = { + users: filteredData, + admins: userTeamRole === TeamRole.ADMIN ? filteredAdmin : teamAdmins, + }; + const userTeams = [...userData.teams]; + const userFilteredTeams = userTeams.filter( + (item) => item.id.toString() !== payload.teamId.toString(), + ); + const userFilteredWorkspaces = userData.workspaces.filter( + (workspace) => workspace.teamId !== payload.teamId, + ); + const userUpdatedParams = { + teams: userFilteredTeams, + workspaces: userFilteredWorkspaces, + }; + await this.downgradeUserRepository.updateUserById( + userFilter, + userUpdatedParams, + ); + const data = await this.downgradeTeamRepository.updateTeamById( + teamFilter, + teamUpdatedParams, + ); + // Update license tracking after user removal + await this.licenseManagementService.updateLicenseTracking(payload.teamId); + return data; + } + + /** + * Get owner details by ID. + * @param {string} ownerId - The ID of the owner. + * @param {any[]} users - Array of users. + * @returns {Promise<{ email: string; name: string } | null>} Owner details or null if not found. + */ + async getOwnerDetails( + ownerId: string, + users: any[], + ): Promise<{ email: string; name: string } | null> { + for (const user of users) { + if (user.id.toString() === ownerId.toString()) { + return { email: user.email, name: user.name }; + } + } + return null; + } + + async deleteWorkspace(id: string): Promise { + const workspace = await this.downgradeWorkspaceReposiory.get(id); + const teamData = await this.downgradeTeamRepository.findTeamByTeamId( + new ObjectId(workspace.team.id), + ); + const teamWorkspaces = [...teamData.workspaces]; + const updatedTeamWorkspaces = teamWorkspaces.filter( + (workspace) => workspace.id.toString() !== id, + ); + const updatedTeamParams = { + workspaces: updatedTeamWorkspaces, + }; + await this.downgradeTeamRepository.updateTeamById( + new ObjectId(workspace.team.id), + updatedTeamParams, + ); + const workspaceUsers = [...workspace.users]; + const updatedIdArray = []; + for (const item of workspaceUsers) { + if (!isString(item.id)) { + updatedIdArray.push(item.id); + continue; + } + updatedIdArray.push(new ObjectId(item.id)); + } + const userDataArray = + await this.downgradeUserRepository.findUsersByIdArray(updatedIdArray); + for (let index = 0; index < userDataArray.length; index++) { + userDataArray[index].workspaces = userDataArray[index].workspaces.filter( + (item: any) => item.workspaceId.toString() !== id, + ); + } + const userDataPromises = []; + for (const item of userDataArray) { + userDataPromises.push( + this.downgradeUserRepository.updateUserById( + new ObjectId(item._id), + item, + ), + ); + } + await Promise.all(userDataPromises); + const data = await this.downgradeWorkspaceReposiory.delete(id); + return data; + } +} diff --git a/src/modules/billing/services/stripe-subscription.service.ts b/src/modules/billing/services/stripe-subscription.service.ts index 288d75347..c5e1219cf 100644 --- a/src/modules/billing/services/stripe-subscription.service.ts +++ b/src/modules/billing/services/stripe-subscription.service.ts @@ -12,9 +12,12 @@ import { BillingEventType, } from "@src/modules/common/enum/billing.enum"; import { PlanName } from "@src/modules/common/enum/plan.enum"; -import { TeamsPlan } from "@src/modules/common/models/team.model"; +import { Team, TeamsPlan } from "@src/modules/common/models/team.model"; import { ScheduledDowngradeDto } from "@src/modules/common/models/billing.model"; import { LicensesDto } from "@src/modules/common/models/licenses.model"; +import { WorkspaceDto } from "@src/modules/common/models/workspace.model"; +import { UserDto } from "@src/modules/common/models/user.model"; +import { DownGradeService } from "./downgrade.service"; // Dynamically import Stripe service class let StripeService: any; @@ -33,6 +36,7 @@ export class StripeSubscriptionService { private readonly stripeSubscriptionRepo: StripeSubscriptionRepository, private readonly billingAuditService: BillingAuditService, private readonly paymentEmailHelper: PaymentEmailHelper, + private readonly downgradeService: DownGradeService, @Optional() @Inject(StripeService) private readonly stripeService?: any, ) {} @@ -93,7 +97,8 @@ export class StripeSubscriptionService { reason: `Subscription created with status: ${subscription.status}`, }, ); - + // Remove auto Disable Auto Downgrade. + await this.stripeSubscriptionRepo.disableAutoDowngrade(metadata.hubId); // Only update team state if subscription is active if (subscription.status === SubscriptionStatus.ACTIVE) { await this.updateTeamAndWorkspacesWithPlan( @@ -141,6 +146,9 @@ export class StripeSubscriptionService { undefined, // no subscription details needed planLimits, // Add plan limits for automatic HUB_LIMIT_UPDATED tracking ); + await this.stripeSubscriptionRepo.removeDowngradeDetails( + metadata.hubId, + ); } } } catch (error) { @@ -918,6 +926,8 @@ export class StripeSubscriptionService { const team = await this.stripeSubscriptionRepo.findTeamById( metadata.hubId, ); + // Execute manual downgrade AFTER plan change is complete + await this.executeManualDowngrade(team, metadata.hubId); const previousPlan = team?.plan?.name || "unknown"; // Get cancellation reason if available @@ -970,6 +980,7 @@ export class StripeSubscriptionService { reason: `Subscription deleted - ${cancellationReason}`, }, ); + await this.stripeSubscriptionRepo.removeDowngradeDetails(metadata.hubId); } catch (error) { throw error; } @@ -1083,6 +1094,9 @@ export class StripeSubscriptionService { scheduledDowngrade: scheduledDowngrade, updatedBy: "system-stripe-webhook", }; + console.log("--------------this is the downgrade update..------->"); + // Execute manual downgrade AFTER plan change is complete + await this.executeManualDowngrade(team, hubId); await this.stripeSubscriptionRepo.updateTeamPlan(hubId, team.plan, { billing: updatedBilling, @@ -1101,6 +1115,7 @@ export class StripeSubscriptionService { console.error("Error sending plan downgrade email:", error); } } + await this.stripeSubscriptionRepo.removeDowngradeDetails(hubId); } catch (error) { throw error; } @@ -1143,18 +1158,18 @@ export class StripeSubscriptionService { } // Find the community plan for downgrade - const communityPlan = - await this.stripeSubscriptionRepo.findPlanByName( - PlanName.COMMUNITY, - ); - if (!communityPlan) { - console.error("Community plan not found"); - continue; - } + // const communityPlan = + // await this.stripeSubscriptionRepo.findPlanByName( + // PlanName.COMMUNITY, + // ); + // if (!communityPlan) { + // console.error("Community plan not found"); + // continue; + // } // Ensure the plan has an ID for the update - communityPlan.id = communityPlan._id; - delete communityPlan._id; + // communityPlan.id = communityPlan._id; + // delete communityPlan._id; // Update billing details for expired subscription const billingDetails = { @@ -1167,11 +1182,11 @@ export class StripeSubscriptionService { }; // Update team to community plan - await this.updateTeamPlanWithBilling( - team._id.toString(), - communityPlan, - billingDetails, - ); + // await this.updateTeamPlanWithBilling( + // team._id.toString(), + // communityPlan, + // billingDetails, + // ); // Send plan downgrade email notification if (this.paymentEmailHelper && team.plan?.name) { @@ -1193,18 +1208,20 @@ export class StripeSubscriptionService { | { previous: Record; new: Record } | undefined; - if (team?.plan?.limits && communityPlan.limits) { + if (team?.plan?.limits) { planLimits = { previous: team.plan.limits, - new: communityPlan.limits, + new: team.plan.limits, }; } - + await this.stripeSubscriptionRepo.enableAutoDowngrade( + team._id.toString(), + ); // Log the plan change await this.billingAuditService.recordPlanChange( team._id.toString(), team.plan?.name || "unknown", - PlanName.COMMUNITY, + team.plan?.name, { actor: { type: BillingActorType.SYSTEM, name: "maintenance-job" }, source: BillingSource.BILLING_MAINTENANCE, @@ -2007,4 +2024,71 @@ export class StripeSubscriptionService { throw error; } } + + /** + * Execute manual downgrade - remove workspaces and users not in the downgrade list + * This should only be called when the plan change is complete + * @param team The team data + * @param hubId The team hub ID + */ + private async executeManualDowngrade( + team: Team, + hubId: string, + ): Promise { + const manualDownGrade = team?.manual_downgrade; + const teamDownGradeWorkspaces = team?.downgrade?.workspaceIds; + const teamDownGradeUsers = team?.downgrade?.userIds; + if (!manualDownGrade) { + return; + } + // Skip if downgrade data is missing + if (!teamDownGradeWorkspaces && !teamDownGradeUsers) { + console.warn( + `Manual downgrade enabled but missing downgrade data for team ${hubId}`, + ); + return; + } + try { + const allWorkspaces = + team?.workspaces?.map((workspace: WorkspaceDto) => + workspace.id.toString(), + ) || []; + const allUsers = + team?.users + ?.filter((user: UserDto) => user.role !== "owner") + .map((user: UserDto) => user.id) || []; + + // Workspaces not in the downgrade list (these will be deleted) + const nonDowngradedWorkspaces = allWorkspaces.filter( + (wsId: string) => !teamDownGradeWorkspaces.includes(wsId), + ); + // Users not in the downgrade list (these will be removed) + const nonDowngradedUsers = allUsers.filter( + (userId: string) => !teamDownGradeUsers.includes(userId), + ); + + // Delete workspaces that are not in the downgrade list + for (const workspaceId of nonDowngradedWorkspaces) { + await this.downgradeService.deleteWorkspace(workspaceId); + console.log(`Deleted workspace ${workspaceId} during manual downgrade`); + } + + // Remove users if teamDownGradeUsers has entries (keep specific users) + if (teamDownGradeUsers.length > 0 && nonDowngradedUsers.length > 0) { + for (const userId of nonDowngradedUsers) { + const payload = { + teamId: hubId, + userId: userId, + }; + await this.downgradeService.removeUserFromTeam(payload); + } + } + console.log(`Manual downgrade completed for team ${hubId}`); + } catch (error) { + console.error( + `Error executing manual downgrade for team ${hubId}:`, + error, + ); + } + } } diff --git a/src/modules/common/models/billing.model.ts b/src/modules/common/models/billing.model.ts index 6ee6b0f8b..cfb6d97f7 100644 --- a/src/modules/common/models/billing.model.ts +++ b/src/modules/common/models/billing.model.ts @@ -179,18 +179,16 @@ export class BillingDto { subscription_expired_email_sent?: boolean | Date; } -export class BilingDtoWithDowngradeDetails extends BillingDto{ +export class DowngradeDetails { @IsArray() - @ValidateNested({ each: true }) - @Type(() => String) @IsOptional() - downgrade_workspaces?:string[]; + @Type(() => String) + workspaceIds?: string[]; @IsArray() - @ValidateNested({ each: true }) - @Type(() => String) @IsOptional() - downgrade_users?:string[]; + @Type(() => String) + userIds?: string[]; } class BillingChangeDto { diff --git a/src/modules/common/models/team.model.ts b/src/modules/common/models/team.model.ts index cf51936ff..b8bf9aea8 100644 --- a/src/modules/common/models/team.model.ts +++ b/src/modules/common/models/team.model.ts @@ -19,7 +19,7 @@ import { UserDto } from "./user.model"; import { ObjectId } from "mongodb"; import { SelectedWorkspaces } from "@src/modules/identity/payloads/teamUser.payload"; import { Plan } from "./plan.model"; -import { BilingDtoWithDowngradeDetails, BillingDto } from "./billing.model"; +import { BillingDto, DowngradeDetails } from "./billing.model"; import { LicensesDto } from "./licenses.model"; export class logoDto { @@ -120,11 +120,24 @@ export class Team { @IsOptional() @IsObject() - billing?: BilingDtoWithDowngradeDetails; + billing?: BillingDto; @IsOptional() @IsObject() licenses?: LicensesDto; + + @IsOptional() + @ValidateNested() + @Type(() => DowngradeDetails) + downgrade?: DowngradeDetails; + + @IsBoolean() + @IsOptional() + manual_downgrade?: boolean; + + @IsBoolean() + @IsOptional() + auto_downgrade?: boolean; } export class TeamWithNewInviteTag extends Team { diff --git a/src/modules/identity/guards/hub-invite.guard.ts b/src/modules/identity/guards/hub-invite.guard.ts index f181e9b98..f13ab2ec2 100644 --- a/src/modules/identity/guards/hub-invite.guard.ts +++ b/src/modules/identity/guards/hub-invite.guard.ts @@ -9,9 +9,7 @@ import { TeamService } from "@src/modules/identity/services/team.service"; @Injectable() export class HubInviteGuard implements CanActivate { - constructor( - private readonly teamService: TeamService, - ) {} + constructor(private readonly teamService: TeamService) {} async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); @@ -30,6 +28,9 @@ export class HubInviteGuard implements CanActivate { }); const planData = userTeam?.plan; + if (!planData.active) { + throw new ForbiddenException("Hub is Restricted."); + } if (teamUserEmails.size > planData?.limits?.usersPerHub?.value + 1) { throw new ForbiddenException("Plan limit reached"); } diff --git a/src/modules/identity/guards/user-limt-guard.ts b/src/modules/identity/guards/user-limt-guard.ts index 73a858839..3ffd3dca0 100644 --- a/src/modules/identity/guards/user-limt-guard.ts +++ b/src/modules/identity/guards/user-limt-guard.ts @@ -29,6 +29,9 @@ export class UserLimitGuard implements CanActivate { } const teamData = await this.teamRepository.get(teamId); + if (!teamData.plan?.active) { + throw new ForbiddenException("Hub is Restricted."); + } if (!teamData || !teamData.users) { throw new ForbiddenException("Team not found or invalid."); } @@ -37,7 +40,7 @@ export class UserLimitGuard implements CanActivate { if (!user) { throw new ForbiddenException("User not found in team."); } - + const status = await this.userLimitService.checkLimitAndLogRequest( user.id, teamId, diff --git a/src/modules/workspace/guards/plan-limits/create-testflow-block-guard.ts b/src/modules/workspace/guards/plan-limits/create-testflow-block-guard.ts index 6c390a50d..1d8d246aa 100644 --- a/src/modules/workspace/guards/plan-limits/create-testflow-block-guard.ts +++ b/src/modules/workspace/guards/plan-limits/create-testflow-block-guard.ts @@ -12,7 +12,7 @@ import { TeamService } from "@src/modules/identity/services/team.service"; export class CreateTestflowBlockGuard implements CanActivate { constructor( private readonly workspaceService: WorkspaceService, - private readonly teamService: TeamService + private readonly teamService: TeamService, ) {} async canActivate(context: ExecutionContext): Promise { @@ -22,8 +22,10 @@ export class CreateTestflowBlockGuard implements CanActivate { ); const teamId = workspaceDetails.team.id; const userTeam = await this.teamService.get(teamId); - const planData = userTeam?.plan - + const planData = userTeam?.plan; + if (!planData.active) { + throw new ForbiddenException("Hub is Restricted."); + } if ( request?.body?.nodes?.length > planData?.limits?.blocksPerTestflow?.value + 1 diff --git a/src/modules/workspace/guards/plan-limits/create-testflow-guard.ts b/src/modules/workspace/guards/plan-limits/create-testflow-guard.ts index e558f6992..e712ec229 100644 --- a/src/modules/workspace/guards/plan-limits/create-testflow-guard.ts +++ b/src/modules/workspace/guards/plan-limits/create-testflow-guard.ts @@ -23,6 +23,9 @@ export class CreateTestflowGuard implements CanActivate { const teamId = workspaceDetails.team.id; const userTeam = await this.teamService.get(teamId); const planData = userTeam?.plan + if (!planData.active) { + throw new ForbiddenException("Hub is Restricted."); + } if ( workspaceDetails?.testflows?.length >= planData?.limits?.testflowPerWorkspace?.value diff --git a/src/modules/workspace/guards/plan-limits/create-testflow-schedule-guard.ts b/src/modules/workspace/guards/plan-limits/create-testflow-schedule-guard.ts index b7d8f18aa..f66f16671 100644 --- a/src/modules/workspace/guards/plan-limits/create-testflow-schedule-guard.ts +++ b/src/modules/workspace/guards/plan-limits/create-testflow-schedule-guard.ts @@ -28,6 +28,9 @@ export class CreateTestflowScheduleGuard implements CanActivate { const testflow = await this.testflowRepository.get( request?.body?.testflowId, ); + if (!planData.active) { + throw new ForbiddenException("Hub is Restricted."); + } if ( Array.isArray(testflow?.schedules) && testflow.schedules.length >= diff --git a/src/modules/workspace/guards/plan-limits/create-workspace-guard.ts b/src/modules/workspace/guards/plan-limits/create-workspace-guard.ts index 9d635f1b7..fc51ec275 100644 --- a/src/modules/workspace/guards/plan-limits/create-workspace-guard.ts +++ b/src/modules/workspace/guards/plan-limits/create-workspace-guard.ts @@ -17,6 +17,9 @@ export class CreateWorkspaceGuard implements CanActivate { const teamId = request?.body?.id; const usersTeamdetails = await this.teamService.get(teamId); const planData = usersTeamdetails?.plan; + if (!planData.active) { + throw new ForbiddenException("Hub is Restricted."); + } if ( usersTeamdetails?.workspaces?.length >= planData?.limits?.workspacesPerHub?.value diff --git a/src/modules/workspace/guards/plan-limits/workspace-invite.guard.ts b/src/modules/workspace/guards/plan-limits/workspace-invite.guard.ts index 7c9c9ede1..c4ad71eea 100644 --- a/src/modules/workspace/guards/plan-limits/workspace-invite.guard.ts +++ b/src/modules/workspace/guards/plan-limits/workspace-invite.guard.ts @@ -35,6 +35,9 @@ export class WorkspaceInviteGuard implements CanActivate { const planData = userTeam?.plan; + if (!planData.active) { + throw new ForbiddenException("Hub is Restricted."); + } if (teamUserEmails.size > planData?.limits?.usersPerHub?.value + 1) { throw new ForbiddenException("Plan limit reached"); } From c9926b543fe126fe1a00417e170269f4e12fb199 Mon Sep 17 00:00:00 2001 From: "aakash.reddy@techdome.net.in" Date: Mon, 13 Oct 2025 12:25:48 +0530 Subject: [PATCH 03/20] feat: manual downgrade setup and auto downgrade --- .../services/stripe-subscription.service.ts | 59 +++++++++++++++---- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/src/modules/billing/services/stripe-subscription.service.ts b/src/modules/billing/services/stripe-subscription.service.ts index c5e1219cf..e7ea70d9d 100644 --- a/src/modules/billing/services/stripe-subscription.service.ts +++ b/src/modules/billing/services/stripe-subscription.service.ts @@ -97,8 +97,6 @@ export class StripeSubscriptionService { reason: `Subscription created with status: ${subscription.status}`, }, ); - // Remove auto Disable Auto Downgrade. - await this.stripeSubscriptionRepo.disableAutoDowngrade(metadata.hubId); // Only update team state if subscription is active if (subscription.status === SubscriptionStatus.ACTIVE) { await this.updateTeamAndWorkspacesWithPlan( @@ -146,9 +144,6 @@ export class StripeSubscriptionService { undefined, // no subscription details needed planLimits, // Add plan limits for automatic HUB_LIMIT_UPDATED tracking ); - await this.stripeSubscriptionRepo.removeDowngradeDetails( - metadata.hubId, - ); } } } catch (error) { @@ -561,6 +556,31 @@ export class StripeSubscriptionService { const isSubscriptionRenewal = !isPlanChange && !isSeatChange && previousPeriodEnd; + if (isPlanChange && previousPlan && newPlan) { + // Get plan details to determine if it's a downgrade + const previousPlanDetails = + await this.stripeSubscriptionRepo.findPlanByName(previousPlan); + const newPlanDetails = + await this.stripeSubscriptionRepo.findPlanByName(newPlan); + + // Simple downgrade detection: if new plan has fewer features/limits or is Community plan + const isDowngrade = this.isPlanDowngrade( + previousPlanDetails, + newPlanDetails, + ); + + if (isDowngrade) { + // Execute manual downgrade updating the plan + await this.executeManualDowngrade(team, metadata.hubId); + await this.stripeSubscriptionRepo.removeDowngradeDetails( + metadata.hubId, + ); + } + } + if (team?.auto_downgrade) { + await this.stripeSubscriptionRepo.disableAutoDowngrade(metadata.hubId); + } + // Create billing details object with successful payment status const billingDetails = { current_period_start: period.start @@ -1094,10 +1114,6 @@ export class StripeSubscriptionService { scheduledDowngrade: scheduledDowngrade, updatedBy: "system-stripe-webhook", }; - console.log("--------------this is the downgrade update..------->"); - // Execute manual downgrade AFTER plan change is complete - await this.executeManualDowngrade(team, hubId); - await this.stripeSubscriptionRepo.updateTeamPlan(hubId, team.plan, { billing: updatedBilling, }); @@ -1115,7 +1131,6 @@ export class StripeSubscriptionService { console.error("Error sending plan downgrade email:", error); } } - await this.stripeSubscriptionRepo.removeDowngradeDetails(hubId); } catch (error) { throw error; } @@ -2070,7 +2085,6 @@ export class StripeSubscriptionService { // Delete workspaces that are not in the downgrade list for (const workspaceId of nonDowngradedWorkspaces) { await this.downgradeService.deleteWorkspace(workspaceId); - console.log(`Deleted workspace ${workspaceId} during manual downgrade`); } // Remove users if teamDownGradeUsers has entries (keep specific users) @@ -2091,4 +2105,27 @@ export class StripeSubscriptionService { ); } } + + /** + * Helper method to determine if a plan change is a downgrade + * @param previousPlan The previous plan object + * @param newPlan The new plan object + * @returns Boolean indicating if this is a downgrade + */ + private isPlanDowngrade(previousPlan: any, newPlan: any): boolean { + // Add your plan hierarchy logic here + // For example, you might have a plan hierarchy like: + // Community < standard < Professional + + const planHierarchy: { [key: string]: number } = { + [PlanName.COMMUNITY]: 0, + [PlanName.STANDARD]: 1, + [PlanName.PROFESSIONAL]: 2, + }; + + const previousLevel = planHierarchy[previousPlan.name] ?? 0; + const newLevel = planHierarchy[newPlan.name] ?? 0; + + return newLevel < previousLevel; + } } From 9a1db1f8ee96b91f81ae15330dbbd5828331a39a Mon Sep 17 00:00:00 2001 From: "aakash.reddy@techdome.net.in" Date: Mon, 13 Oct 2025 17:10:51 +0530 Subject: [PATCH 04/20] feat: updated downgrade model and approach --- .../billing/controllers/stripe.controller.ts | 11 +-- .../billing/payloads/stripe.payload.ts | 34 ++++++---- .../downgradeWorkspace.repository.ts | 21 ++++++ .../stripe-subscription.repository.ts | 68 +++++++++++-------- .../billing/services/downgrade.service.ts | 48 ++----------- .../services/stripe-subscription.service.ts | 63 ++++++++++++----- src/modules/common/enum/billing.enum.ts | 5 ++ src/modules/common/models/billing.model.ts | 33 +++++++-- src/modules/common/models/team.model.ts | 8 --- src/modules/common/models/workspace.model.ts | 4 ++ 10 files changed, 177 insertions(+), 118 deletions(-) diff --git a/src/modules/billing/controllers/stripe.controller.ts b/src/modules/billing/controllers/stripe.controller.ts index 6c2b33e2b..cc01d2f7d 100644 --- a/src/modules/billing/controllers/stripe.controller.ts +++ b/src/modules/billing/controllers/stripe.controller.ts @@ -50,6 +50,7 @@ import { ConfigService } from "@nestjs/config"; import { TrialType } from "@src/modules/common/enum/trial.enum"; import { PricingPlan } from "@src/modules/common/models/pricing.model"; import { StripeSubscriptionRepository } from "../repositories/stripe-subscription.repository"; +import { SubscriptionDowngradeType } from "@src/modules/common/enum/billing.enum"; // Dynamically import Stripe services let StripeService: any; @@ -428,8 +429,9 @@ export class StripeController { if (subscription) { await this.stripeSubscriptionRepository.addDowngradeDetails( updateSubscriptionDto?.metadata?.hubId, - updateSubscriptionDto?.workspaceIds, - updateSubscriptionDto?.userIds, + updateSubscriptionDto?.workspaces, + updateSubscriptionDto?.users, + SubscriptionDowngradeType.MANUAL ); } @@ -475,8 +477,9 @@ export class StripeController { if (subscription) { await this.stripeSubscriptionRepository.addDowngradeDetails( cancelSubscriptionDto.teamId, - cancelSubscriptionDto.workspaceIds, - cancelSubscriptionDto.userIds, + cancelSubscriptionDto.workspaces, + cancelSubscriptionDto.users, + SubscriptionDowngradeType.MANUAL ); } return { subscription }; diff --git a/src/modules/billing/payloads/stripe.payload.ts b/src/modules/billing/payloads/stripe.payload.ts index 98ae9c027..90558e288 100644 --- a/src/modules/billing/payloads/stripe.payload.ts +++ b/src/modules/billing/payloads/stripe.payload.ts @@ -1,4 +1,8 @@ import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { + downgradeUser, + downgradeWorkspace, +} from "@src/modules/common/models/billing.model"; import { IsEmail, IsString, @@ -202,19 +206,27 @@ export class UpdateSubscriptionDto { seats?: number; paymentBehavior?: "default_incomplete" | "allow_incomplete"; + @ApiPropertyOptional({ + description: "List of workspaces to downgrade", + example: [ + { id: "workspace-id-1", name: "Workspace One" }, + { id: "workspace-id-2", name: "Workspace Two" }, + ], + }) @IsOptional() @IsArray() - @IsString({ each: true }) - workspaceIds?: string[]; + workspaces?: downgradeWorkspace[]; @ApiPropertyOptional({ - description: "List of user IDs to downgrade", - example: ["example@gmail.com"], + description: "List of users to downgrade", + example: [ + { id: "user-id-1", email: "example1@gmail.com" }, + { id: "user-id-2", email: "example2@gmail.com" }, + ], }) @IsOptional() @IsArray() - @IsString({ each: true }) - userIds?: string[]; + users?: downgradeUser[]; } export class CancelSubscriptionDto { @@ -235,14 +247,9 @@ export class CancelSubscriptionDto { @IsString() teamId?: string; - @ApiPropertyOptional({ - description: "List of workspace IDs to downgrade", - example: ["6889ff80b1338511b5bfc210"], - }) @IsOptional() @IsArray() - @IsString({ each: true }) - workspaceIds?: string[]; + workspaces?: downgradeWorkspace[]; @ApiPropertyOptional({ description: "List of user IDs to downgrade", @@ -250,8 +257,7 @@ export class CancelSubscriptionDto { }) @IsOptional() @IsArray() - @IsString({ each: true }) - userIds?: string[]; + users?: downgradeUser[]; } export class ReactivateSubscriptionDto { diff --git a/src/modules/billing/repositories/downgradeWorkspace.repository.ts b/src/modules/billing/repositories/downgradeWorkspace.repository.ts index 2850ef668..23aa7c1a2 100644 --- a/src/modules/billing/repositories/downgradeWorkspace.repository.ts +++ b/src/modules/billing/repositories/downgradeWorkspace.repository.ts @@ -28,4 +28,25 @@ export class DownGradeWorkspaceRepository { .collection(Collections.WORKSPACE) .deleteOne({ _id }); } + + async setWorkspaceRestriction( + workspaceId: string, + isRestricted: boolean, + ): Promise> { + const _id = new ObjectId(workspaceId); + + const result = await this.db + .collection(Collections.WORKSPACE) + .findOneAndUpdate( + { _id }, + { $set: { isRestricted, updatedAt: new Date() } }, + { returnDocument: "after" }, + ); + + if (!result.value) { + throw new BadRequestException("Workspace not found or update failed"); + } + + return result.value; + } } diff --git a/src/modules/billing/repositories/stripe-subscription.repository.ts b/src/modules/billing/repositories/stripe-subscription.repository.ts index da6079f33..e19a2bacb 100644 --- a/src/modules/billing/repositories/stripe-subscription.repository.ts +++ b/src/modules/billing/repositories/stripe-subscription.repository.ts @@ -3,6 +3,7 @@ import { Collections } from "@src/modules/common/enum/database.collection.enum"; import { BillingType, PaymentProvider, + SubscriptionDowngradeType, SubscriptionStatus, } from "@src/modules/common/enum/billing.enum"; import { Db, ObjectId, UpdateResult } from "mongodb"; @@ -248,20 +249,22 @@ export class StripeSubscriptionRepository { } /** - * Add downgrade details (workspaces and users) to a team's billing record. - * This function updates the billing section of a team document by appending - * workspace IDs to `billing.downgrade.workspaces` and user IDs to - * `billing.downgrade.users`. Duplicate entries are automatically avoided + * Add downgrade details (workspaces and users) to a team's downgrade record. + * This function updates the downgrade section of a team document by appending + * workspace objects to `downgrade.workspaces` and user objects to + * `downgrade.users`. Duplicate entries are automatically avoided * using MongoDB's `$addToSet` operator. - * @param teamId The unique identifier of the team whose billing record will be updated. - * @param workspaceIds Array of workspace IDs to mark for downgrade. - * @param userIds Array of user IDs to mark for downgrade. + * @param teamId The unique identifier of the team whose downgrade record will be updated. + * @param workspaces Array of workspace objects {id, name} to mark for downgrade. + * @param users Array of user objects {id, email} to mark for downgrade. + * @param downgradeType Type of downgrade (manual or auto) * @returns MongoDB UpdateResult indicating the success or failure of the update. */ async addDowngradeDetails( teamId: string, - workspaceIds: string[], - userIds: string[], + workspaces: Array<{ id: string; name: string }>, + users: Array<{ id: string; email: string }>, + downgradeType?: SubscriptionDowngradeType, ): Promise { try { if (!teamId) { @@ -269,26 +272,30 @@ export class StripeSubscriptionRepository { } const updateQuery: Record = {}; // Add workspaces if provided - if (workspaceIds && workspaceIds.length > 0) { - updateQuery["downgrade.workspaceIds"] = { $each: workspaceIds }; + if (workspaces && workspaces.length > 0) { + updateQuery["downgrade.workspaces"] = { $each: workspaces }; } // Add users if provided - if (userIds && userIds.length > 0) { - updateQuery["downgrade.userIds"] = { $each: userIds }; + if (users && users.length > 0) { + updateQuery["downgrade.users"] = { $each: users }; } if (Object.keys(updateQuery).length === 0) { - throw new Error("No workspaceIds or userIds provided to update."); + throw new Error("No workspaces or users provided to update."); } const teamObjectId = new ObjectId(teamId); + const setQuery: Record = { + updatedBy: "system", + updatedAt: new Date(), + }; + // Set downgrade type if provided + if (downgradeType) { + setQuery["downgrade.downgradeType"] = downgradeType; + } const result = await this.db.collection(Collections.TEAM).updateOne( { _id: teamObjectId }, { $addToSet: updateQuery, - $set: { - updatedBy: "system", - manual_downgrade: true, - updatedAt: new Date(), - }, + $set: setQuery, }, ); return result; @@ -298,6 +305,12 @@ export class StripeSubscriptionRepository { } } + /** + * Remove downgrade details from a team + * Removes the entire downgrade object from the team document + * @param teamId The team ID to update + * @returns MongoDB UpdateResult + */ async removeDowngradeDetails(teamId: string): Promise { try { if (!teamId) { @@ -311,7 +324,6 @@ export class StripeSubscriptionRepository { downgrade: "", // Removes the entire downgrade object }, $set: { - manual_downgrade: false, updatedBy: "system", updatedAt: new Date(), }, @@ -326,9 +338,8 @@ export class StripeSubscriptionRepository { /** * Enable automated downgrade for a team - * Sets auto_downgrade flag and optionally updates plan active state + * Sets downgrade type to auto and optionally updates plan active state * @param teamId - The team ID to update - * @param planActive - Optional boolean to set plan.active state * @returns UpdateResult from MongoDB */ async enableAutoDowngrade(teamId: string): Promise { @@ -336,13 +347,13 @@ export class StripeSubscriptionRepository { if (!teamId) { throw new Error("teamId is required to enable auto downgrade."); } + const teamObjectId = new ObjectId(teamId); const setQuery: Record = { - auto_downgrade: true, + "downgrade.downgradeType": SubscriptionDowngradeType.AUTOMATIC, + "plan.active": false, updatedBy: "system", updatedAt: new Date(), }; - setQuery["plan.active"] = false; - const teamObjectId = new ObjectId(teamId); const result = await this.db .collection(Collections.TEAM) .updateOne({ _id: teamObjectId }, { $set: setQuery }); @@ -355,7 +366,7 @@ export class StripeSubscriptionRepository { /** * Disable automated downgrade for a team - * Clears auto_downgrade flag + * Clears downgrade type and sets plan to active * @param teamId - The team ID to update * @returns UpdateResult from MongoDB */ @@ -368,8 +379,11 @@ export class StripeSubscriptionRepository { const result = await this.db.collection(Collections.TEAM).updateOne( { _id: teamObjectId }, { + $unset: { + "downgrade.downgradeType": "", + }, $set: { - auto_downgrade: false, + "plan.active": true, updatedBy: "system", updatedAt: new Date(), }, diff --git a/src/modules/billing/services/downgrade.service.ts b/src/modules/billing/services/downgrade.service.ts index e5f86c784..af8ca46df 100644 --- a/src/modules/billing/services/downgrade.service.ts +++ b/src/modules/billing/services/downgrade.service.ts @@ -88,49 +88,9 @@ export class DownGradeService { return null; } - async deleteWorkspace(id: string): Promise { - const workspace = await this.downgradeWorkspaceReposiory.get(id); - const teamData = await this.downgradeTeamRepository.findTeamByTeamId( - new ObjectId(workspace.team.id), - ); - const teamWorkspaces = [...teamData.workspaces]; - const updatedTeamWorkspaces = teamWorkspaces.filter( - (workspace) => workspace.id.toString() !== id, - ); - const updatedTeamParams = { - workspaces: updatedTeamWorkspaces, - }; - await this.downgradeTeamRepository.updateTeamById( - new ObjectId(workspace.team.id), - updatedTeamParams, - ); - const workspaceUsers = [...workspace.users]; - const updatedIdArray = []; - for (const item of workspaceUsers) { - if (!isString(item.id)) { - updatedIdArray.push(item.id); - continue; - } - updatedIdArray.push(new ObjectId(item.id)); - } - const userDataArray = - await this.downgradeUserRepository.findUsersByIdArray(updatedIdArray); - for (let index = 0; index < userDataArray.length; index++) { - userDataArray[index].workspaces = userDataArray[index].workspaces.filter( - (item: any) => item.workspaceId.toString() !== id, - ); - } - const userDataPromises = []; - for (const item of userDataArray) { - userDataPromises.push( - this.downgradeUserRepository.updateUserById( - new ObjectId(item._id), - item, - ), - ); - } - await Promise.all(userDataPromises); - const data = await this.downgradeWorkspaceReposiory.delete(id); - return data; + async restrictWorkspace(id: string): Promise { + const response = + await this.downgradeWorkspaceReposiory.setWorkspaceRestriction(id, true); + return response; } } diff --git a/src/modules/billing/services/stripe-subscription.service.ts b/src/modules/billing/services/stripe-subscription.service.ts index e7ea70d9d..f341e7f3d 100644 --- a/src/modules/billing/services/stripe-subscription.service.ts +++ b/src/modules/billing/services/stripe-subscription.service.ts @@ -10,6 +10,7 @@ import { BillingActorType, BillingSource, BillingEventType, + SubscriptionDowngradeType, } from "@src/modules/common/enum/billing.enum"; import { PlanName } from "@src/modules/common/enum/plan.enum"; import { Team, TeamsPlan } from "@src/modules/common/models/team.model"; @@ -195,6 +196,7 @@ export class StripeSubscriptionService { metadata: any, eventId?: string, ): Promise { + console.log("this is the subscription of cancekk------->"); // Get current team state const currentTeam = await this.stripeSubscriptionRepo.findTeamById( metadata.hubId, @@ -2050,54 +2052,81 @@ export class StripeSubscriptionService { team: Team, hubId: string, ): Promise { - const manualDownGrade = team?.manual_downgrade; - const teamDownGradeWorkspaces = team?.downgrade?.workspaceIds; - const teamDownGradeUsers = team?.downgrade?.userIds; - if (!manualDownGrade) { + const downgrade = team?.downgrade; + if ( + !downgrade || + downgrade.downgradeType !== SubscriptionDowngradeType.MANUAL + ) { + return; + } + const downgradeType = team?.downgrade?.downgradeType; + const teamDowngradeWorkspaces = team?.downgrade?.workspaces; + const teamDowngradeUsers = team?.downgrade?.users; + // Only proceed if downgrade type is MANUAL + if (downgradeType !== SubscriptionDowngradeType.MANUAL) { return; } // Skip if downgrade data is missing - if (!teamDownGradeWorkspaces && !teamDownGradeUsers) { + if (!teamDowngradeWorkspaces && !teamDowngradeUsers) { console.warn( `Manual downgrade enabled but missing downgrade data for team ${hubId}`, ); return; } try { + // Get all current workspace IDs const allWorkspaces = team?.workspaces?.map((workspace: WorkspaceDto) => workspace.id.toString(), ) || []; + // Get all current user IDs (excluding owner) const allUsers = team?.users ?.filter((user: UserDto) => user.role !== "owner") .map((user: UserDto) => user.id) || []; + // Extract workspace IDs from downgrade list (workspaces to keep) + const downgradeWorkspaceIds = + teamDowngradeWorkspaces?.map((ws) => ws.id) || []; + // Extract user IDs from downgrade list (users to keep) + const downgradeUserIds = teamDowngradeUsers?.map((user) => user.id) || []; // Workspaces not in the downgrade list (these will be deleted) const nonDowngradedWorkspaces = allWorkspaces.filter( - (wsId: string) => !teamDownGradeWorkspaces.includes(wsId), + (wsId: string) => !downgradeWorkspaceIds.includes(wsId), ); // Users not in the downgrade list (these will be removed) const nonDowngradedUsers = allUsers.filter( - (userId: string) => !teamDownGradeUsers.includes(userId), + (userId: string) => !downgradeUserIds.includes(userId), ); // Delete workspaces that are not in the downgrade list - for (const workspaceId of nonDowngradedWorkspaces) { - await this.downgradeService.deleteWorkspace(workspaceId); + if (nonDowngradedWorkspaces.length > 0) { + for (const workspaceId of nonDowngradedWorkspaces) { + await this.downgradeService.restrictWorkspace(workspaceId); + } } - // Remove users if teamDownGradeUsers has entries (keep specific users) - if (teamDownGradeUsers.length > 0 && nonDowngradedUsers.length > 0) { + // Remove users not in the downgrade list + if (nonDowngradedUsers.length > 0) { for (const userId of nonDowngradedUsers) { - const payload = { - teamId: hubId, - userId: userId, - }; - await this.downgradeService.removeUserFromTeam(payload); + try { + const payload = { + teamId: hubId, + userId: userId, + }; + await this.downgradeService.removeUserFromTeam(payload); + } catch (error) { + console.error( + `Error removing user ${userId} from team ${hubId}:`, + error, + ); + // Continue with other users even if one fails + } } } - console.log(`Manual downgrade completed for team ${hubId}`); + console.log( + `Manual downgrade completed for team ${hubId}: ${nonDowngradedWorkspaces.length} workspaces deleted, ${nonDowngradedUsers.length} users removed`, + ); } catch (error) { console.error( `Error executing manual downgrade for team ${hubId}:`, diff --git a/src/modules/common/enum/billing.enum.ts b/src/modules/common/enum/billing.enum.ts index 72a7eb62f..f5613f0da 100644 --- a/src/modules/common/enum/billing.enum.ts +++ b/src/modules/common/enum/billing.enum.ts @@ -137,3 +137,8 @@ export enum BillingTransactionType { ADJUSTMENT = "adjustment", PRORATION = "proration", } + +export enum SubscriptionDowngradeType { + MANUAL = "manual", + AUTOMATIC = "auto", +} diff --git a/src/modules/common/models/billing.model.ts b/src/modules/common/models/billing.model.ts index cfb6d97f7..32b40e493 100644 --- a/src/modules/common/models/billing.model.ts +++ b/src/modules/common/models/billing.model.ts @@ -16,6 +16,7 @@ import { BillingEventType, BillingSource, BillingTransactionType, + SubscriptionDowngradeType, } from "../enum/billing.enum"; export class PaymentProviderDto { @@ -182,13 +183,37 @@ export class BillingDto { export class DowngradeDetails { @IsArray() @IsOptional() - @Type(() => String) - workspaceIds?: string[]; + @Type(() => downgradeWorkspace) + workspaces?: downgradeWorkspace[]; @IsArray() @IsOptional() - @Type(() => String) - userIds?: string[]; + @Type(() => downgradeUser) + users?: downgradeUser[]; + + @IsString() + @IsOptional() + downgradeType?: SubscriptionDowngradeType; +} + +export class downgradeWorkspace { + @IsString() + @IsOptional() + id: string; + + @IsString() + @IsOptional() + name: string; +} + +export class downgradeUser { + @IsString() + @IsOptional() + id: string; + + @IsString() + @IsOptional() + email: string; } class BillingChangeDto { diff --git a/src/modules/common/models/team.model.ts b/src/modules/common/models/team.model.ts index b8bf9aea8..f835c6634 100644 --- a/src/modules/common/models/team.model.ts +++ b/src/modules/common/models/team.model.ts @@ -130,14 +130,6 @@ export class Team { @ValidateNested() @Type(() => DowngradeDetails) downgrade?: DowngradeDetails; - - @IsBoolean() - @IsOptional() - manual_downgrade?: boolean; - - @IsBoolean() - @IsOptional() - auto_downgrade?: boolean; } export class TeamWithNewInviteTag extends Team { diff --git a/src/modules/common/models/workspace.model.ts b/src/modules/common/models/workspace.model.ts index f53b1d053..ef19e8761 100644 --- a/src/modules/common/models/workspace.model.ts +++ b/src/modules/common/models/workspace.model.ts @@ -126,6 +126,10 @@ export class Workspace { @IsOptional() users?: UserDto[]; + @IsBoolean() + @IsOptional() + isRestricted?: boolean; + @IsDate() @IsOptional() createdAt?: Date; From c0827236238ce2c62be0aaad5f3e12083c523b8e Mon Sep 17 00:00:00 2001 From: "aakash.reddy@techdome.net.in" Date: Mon, 13 Oct 2025 17:49:57 +0530 Subject: [PATCH 05/20] feat: updated downgrade model and approach --- .../billing/controllers/stripe.controller.ts | 13 ++++----- .../billing/services/downgrade.service.ts | 28 +++++++++++++++++++ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/modules/billing/controllers/stripe.controller.ts b/src/modules/billing/controllers/stripe.controller.ts index cc01d2f7d..a3ce1e4ca 100644 --- a/src/modules/billing/controllers/stripe.controller.ts +++ b/src/modules/billing/controllers/stripe.controller.ts @@ -49,8 +49,7 @@ import { SalesEmailRepository } from "@src/modules/workspace/repositories/sales- import { ConfigService } from "@nestjs/config"; import { TrialType } from "@src/modules/common/enum/trial.enum"; import { PricingPlan } from "@src/modules/common/models/pricing.model"; -import { StripeSubscriptionRepository } from "../repositories/stripe-subscription.repository"; -import { SubscriptionDowngradeType } from "@src/modules/common/enum/billing.enum"; +import { DownGradeService } from "../services/downgrade.service"; // Dynamically import Stripe services let StripeService: any; @@ -74,7 +73,7 @@ export class StripeController { private readonly pricingService: PricingService, private readonly configService: ConfigService, private readonly salesEmailRepository: SalesEmailRepository, - private readonly stripeSubscriptionRepository: StripeSubscriptionRepository, + private readonly downgradeService: DownGradeService, ) { this.isStripeAvailable = !!this.stripeService; @@ -427,11 +426,10 @@ export class StripeController { updateSubscriptionDto.paymentBehavior, ); if (subscription) { - await this.stripeSubscriptionRepository.addDowngradeDetails( + await this.downgradeService.addDowgradeDetails( updateSubscriptionDto?.metadata?.hubId, updateSubscriptionDto?.workspaces, updateSubscriptionDto?.users, - SubscriptionDowngradeType.MANUAL ); } @@ -475,11 +473,10 @@ export class StripeController { false, //disables cancellation at mid cycle ); if (subscription) { - await this.stripeSubscriptionRepository.addDowngradeDetails( + await this.downgradeService.addDowgradeDetails( cancelSubscriptionDto.teamId, cancelSubscriptionDto.workspaces, cancelSubscriptionDto.users, - SubscriptionDowngradeType.MANUAL ); } return { subscription }; @@ -527,7 +524,7 @@ export class StripeController { reactivateDto.metadata, ); if (subscription) { - await this.stripeSubscriptionRepository.removeDowngradeDetails( + await this.downgradeService.removeDowngradeDetails( reactivateDto.metadata?.hubId, ); } diff --git a/src/modules/billing/services/downgrade.service.ts b/src/modules/billing/services/downgrade.service.ts index af8ca46df..cced46bf3 100644 --- a/src/modules/billing/services/downgrade.service.ts +++ b/src/modules/billing/services/downgrade.service.ts @@ -8,6 +8,8 @@ import { DownGradeUserRepository } from "../repositories/downgradeUser.repositor import { LicenseManagementService } from "./license-management.service"; import { DownGradeWorkspaceRepository } from "../repositories/downgradeWorkspace.repository"; import { isString } from "class-validator"; +import { SubscriptionDowngradeType } from "@src/modules/common/enum/billing.enum"; +import { StripeSubscriptionRepository } from "../repositories/stripe-subscription.repository"; @Injectable() export class DownGradeService { @@ -16,6 +18,7 @@ export class DownGradeService { private readonly downgradeUserRepository: DownGradeUserRepository, private readonly licenseManagementService: LicenseManagementService, private readonly downgradeWorkspaceReposiory: DownGradeWorkspaceRepository, + private readonly stripeSubscriptionRepository: StripeSubscriptionRepository, ) {} async removeUserFromTeam(payload: DowngradeUserDto): Promise> { @@ -93,4 +96,29 @@ export class DownGradeService { await this.downgradeWorkspaceReposiory.setWorkspaceRestriction(id, true); return response; } + + async addDowgradeDetails( + teamId: string, + workspaces: Array<{ id: string; name: string }>, + users: Array<{ id: string; email: string }>, + ) { + try { + await this.stripeSubscriptionRepository.addDowngradeDetails( + teamId, + workspaces, + users, + SubscriptionDowngradeType.MANUAL, + ); + } catch (error) { + console.log("Error in add Downgrade", error); + } + } + + async removeDowngradeDetails(teamId: string) { + try { + await this.stripeSubscriptionRepository.removeDowngradeDetails(teamId); + } catch (error) { + console.log("Error in Remove Downgrade", error); + } + } } From 8198b1a49e61810cab3370458ca4b21d251d64aa Mon Sep 17 00:00:00 2001 From: "aakash.reddy@techdome.net.in" Date: Mon, 13 Oct 2025 18:07:04 +0530 Subject: [PATCH 06/20] feat: updated downgrade model and approach --- src/modules/billing/services/stripe-subscription.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/billing/services/stripe-subscription.service.ts b/src/modules/billing/services/stripe-subscription.service.ts index f341e7f3d..4c2934822 100644 --- a/src/modules/billing/services/stripe-subscription.service.ts +++ b/src/modules/billing/services/stripe-subscription.service.ts @@ -579,7 +579,7 @@ export class StripeSubscriptionService { ); } } - if (team?.auto_downgrade) { + if (team?.downgrade.downgradeType === SubscriptionDowngradeType.AUTOMATIC) { await this.stripeSubscriptionRepo.disableAutoDowngrade(metadata.hubId); } From 7b37d81a2eac13a0dafa6fd88b18e32451471d1b Mon Sep 17 00:00:00 2001 From: "aakash.reddy@techdome.net.in" Date: Mon, 13 Oct 2025 20:41:42 +0530 Subject: [PATCH 07/20] fix: email template link issue --- .../billing/services/downgrade.service.ts | 38 +++++++++++++++++++ .../services/stripe-subscription.service.ts | 7 +++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/modules/billing/services/downgrade.service.ts b/src/modules/billing/services/downgrade.service.ts index cced46bf3..720233a10 100644 --- a/src/modules/billing/services/downgrade.service.ts +++ b/src/modules/billing/services/downgrade.service.ts @@ -91,12 +91,26 @@ export class DownGradeService { return null; } + /** + * Restrict a specific workspace by its ID. + * + * @param id - The unique identifier of the workspace to restrict. + */ async restrictWorkspace(id: string): Promise { const response = await this.downgradeWorkspaceReposiory.setWorkspaceRestriction(id, true); return response; } + /** + * Add downgrade details for a specific team. + * @param teamId - The unique identifier of the team being downgraded. + * @param workspaces - An array of workspace objects (each containing `id` and `name`) + * associated with the downgrade. + * @param users - An array of user objects (each containing `id` and `email`) + * affected by the downgrade. + * associated workspaces and users. + */ async addDowgradeDetails( teamId: string, workspaces: Array<{ id: string; name: string }>, @@ -114,6 +128,11 @@ export class DownGradeService { } } + /** + * Remove existing downgrade details for a team. + * + * @param teamId - The unique identifier of the team whose downgrade details are to be removed. + */ async removeDowngradeDetails(teamId: string) { try { await this.stripeSubscriptionRepository.removeDowngradeDetails(teamId); @@ -121,4 +140,23 @@ export class DownGradeService { console.log("Error in Remove Downgrade", error); } } + + /** + * unRestrict a specific Hub. + * @param id - The unique identifier of the workspace to restrict. + * @returns A promise resolving to the repository response after setting the restriction. + **/ + async unRestrictWorkpsaces(team: Team) { + try { + const workspaces = team.workspaces; + for (let workspace of workspaces) { + await this.downgradeWorkspaceReposiory.setWorkspaceRestriction( + workspace.id.toString(), + false, + ); + } + } catch (error) { + console.log("Error in Removing restricted Workspaces." + error); + } + } } diff --git a/src/modules/billing/services/stripe-subscription.service.ts b/src/modules/billing/services/stripe-subscription.service.ts index 4c2934822..71492d395 100644 --- a/src/modules/billing/services/stripe-subscription.service.ts +++ b/src/modules/billing/services/stripe-subscription.service.ts @@ -618,7 +618,12 @@ export class StripeSubscriptionService { ), }; - await this.updateTeamPlanWithBilling(metadata.hubId, plan, billingDetails); + const updateTeam = await this.updateTeamPlanWithBilling( + metadata.hubId, + plan, + billingDetails, + ); + await this.downgradeService.unRestrictWorkpsaces(updateTeam); // Update team with new license data await this.stripeSubscriptionRepo.updateTeamById(metadata.hubId, { licenses: licenseUpdate, From 6efe685c3135943984088c192ddfac0feb39881e Mon Sep 17 00:00:00 2001 From: "aakash.reddy@techdome.net.in" Date: Mon, 13 Oct 2025 23:13:51 +0530 Subject: [PATCH 08/20] feat: manual downgrade condition added --- .../billing/controllers/stripe.controller.ts | 2 +- .../services/stripe-subscription.service.ts | 42 +++++++++++++------ 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/modules/billing/controllers/stripe.controller.ts b/src/modules/billing/controllers/stripe.controller.ts index a3ce1e4ca..e3b248583 100644 --- a/src/modules/billing/controllers/stripe.controller.ts +++ b/src/modules/billing/controllers/stripe.controller.ts @@ -425,7 +425,7 @@ export class StripeController { updateSubscriptionDto.seats, updateSubscriptionDto.paymentBehavior, ); - if (subscription) { + if (subscription && updateSubscriptionDto?.workspaces) { await this.downgradeService.addDowgradeDetails( updateSubscriptionDto?.metadata?.hubId, updateSubscriptionDto?.workspaces, diff --git a/src/modules/billing/services/stripe-subscription.service.ts b/src/modules/billing/services/stripe-subscription.service.ts index 71492d395..739606301 100644 --- a/src/modules/billing/services/stripe-subscription.service.ts +++ b/src/modules/billing/services/stripe-subscription.service.ts @@ -557,7 +557,8 @@ export class StripeSubscriptionService { const isSeatChange = previousSeats !== newSeats; const isSubscriptionRenewal = !isPlanChange && !isSeatChange && previousPeriodEnd; - + let isDowngrading = false; + // Only check for downgrade if there's a plan change if (isPlanChange && previousPlan && newPlan) { // Get plan details to determine if it's a downgrade const previousPlanDetails = @@ -565,21 +566,29 @@ export class StripeSubscriptionService { const newPlanDetails = await this.stripeSubscriptionRepo.findPlanByName(newPlan); - // Simple downgrade detection: if new plan has fewer features/limits or is Community plan + // Check if this is actually a downgrade (moving to a lower tier) const isDowngrade = this.isPlanDowngrade( previousPlanDetails, newPlanDetails, ); if (isDowngrade) { - // Execute manual downgrade updating the plan - await this.executeManualDowngrade(team, metadata.hubId); - await this.stripeSubscriptionRepo.removeDowngradeDetails( - metadata.hubId, - ); + const hasDowngradeConfig = team?.downgrade; + const isManualDowngrade = + team?.downgrade?.downgradeType === SubscriptionDowngradeType.MANUAL; + if (hasDowngradeConfig && isManualDowngrade) { + isDowngrading = true; + await this.executeManualDowngrade(team, metadata.hubId); + await this.stripeSubscriptionRepo.removeDowngradeDetails( + metadata.hubId, + ); + } } } - if (team?.downgrade.downgradeType === SubscriptionDowngradeType.AUTOMATIC) { + // Handle automatic downgrade cleanup (separate from manual downgrade) + if ( + team?.downgrade?.downgradeType === SubscriptionDowngradeType.AUTOMATIC + ) { await this.stripeSubscriptionRepo.disableAutoDowngrade(metadata.hubId); } @@ -623,7 +632,9 @@ export class StripeSubscriptionService { plan, billingDetails, ); - await this.downgradeService.unRestrictWorkpsaces(updateTeam); + if (!isDowngrading) { + await this.downgradeService.unRestrictWorkpsaces(updateTeam); + } // Update team with new license data await this.stripeSubscriptionRepo.updateTeamById(metadata.hubId, { licenses: licenseUpdate, @@ -2060,7 +2071,8 @@ export class StripeSubscriptionService { const downgrade = team?.downgrade; if ( !downgrade || - downgrade.downgradeType !== SubscriptionDowngradeType.MANUAL + (downgrade?.downgradeType && + downgrade?.downgradeType !== SubscriptionDowngradeType.MANUAL) ) { return; } @@ -2072,12 +2084,16 @@ export class StripeSubscriptionService { return; } // Skip if downgrade data is missing - if (!teamDowngradeWorkspaces && !teamDowngradeUsers) { + if (!teamDowngradeWorkspaces) { console.warn( - `Manual downgrade enabled but missing downgrade data for team ${hubId}`, + `Manual downgrade enabled but missing downgrade data for team ${hubId}.`, ); return; } + if (teamDowngradeWorkspaces.length < 1) { + console.warn(`Manual downgrade Workspaces are Missing for ${hubId}.`); + return; + } try { // Get all current workspace IDs const allWorkspaces = @@ -2112,7 +2128,7 @@ export class StripeSubscriptionService { } // Remove users not in the downgrade list - if (nonDowngradedUsers.length > 0) { + if (nonDowngradedUsers.length > 0 && teamDowngradeUsers.length > 0) { for (const userId of nonDowngradedUsers) { try { const payload = { From a6fc0d1fec8adc9fd569b7f6c925cae1e7443b5b Mon Sep 17 00:00:00 2001 From: "aakash.reddy@techdome.net.in" Date: Tue, 14 Oct 2025 12:08:03 +0530 Subject: [PATCH 09/20] fix: setup of restoring workspaces --- .../repositories/downgradeWorkspace.repository.ts | 3 --- .../billing/services/stripe-subscription.service.ts | 11 ++++++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/modules/billing/repositories/downgradeWorkspace.repository.ts b/src/modules/billing/repositories/downgradeWorkspace.repository.ts index 23aa7c1a2..952b93b4f 100644 --- a/src/modules/billing/repositories/downgradeWorkspace.repository.ts +++ b/src/modules/billing/repositories/downgradeWorkspace.repository.ts @@ -34,7 +34,6 @@ export class DownGradeWorkspaceRepository { isRestricted: boolean, ): Promise> { const _id = new ObjectId(workspaceId); - const result = await this.db .collection(Collections.WORKSPACE) .findOneAndUpdate( @@ -42,11 +41,9 @@ export class DownGradeWorkspaceRepository { { $set: { isRestricted, updatedAt: new Date() } }, { returnDocument: "after" }, ); - if (!result.value) { throw new BadRequestException("Workspace not found or update failed"); } - return result.value; } } diff --git a/src/modules/billing/services/stripe-subscription.service.ts b/src/modules/billing/services/stripe-subscription.service.ts index 739606301..2b8c9563d 100644 --- a/src/modules/billing/services/stripe-subscription.service.ts +++ b/src/modules/billing/services/stripe-subscription.service.ts @@ -19,6 +19,8 @@ import { LicensesDto } from "@src/modules/common/models/licenses.model"; import { WorkspaceDto } from "@src/modules/common/models/workspace.model"; import { UserDto } from "@src/modules/common/models/user.model"; import { DownGradeService } from "./downgrade.service"; +import { DownGradeTeamRepository } from "../repositories/downgradeTeam.reposiotry"; +import { ObjectId } from "mongodb"; // Dynamically import Stripe service class let StripeService: any; @@ -38,6 +40,7 @@ export class StripeSubscriptionService { private readonly billingAuditService: BillingAuditService, private readonly paymentEmailHelper: PaymentEmailHelper, private readonly downgradeService: DownGradeService, + private readonly downgradeTeamRepository: DownGradeTeamRepository, @Optional() @Inject(StripeService) private readonly stripeService?: any, ) {} @@ -627,12 +630,10 @@ export class StripeSubscriptionService { ), }; - const updateTeam = await this.updateTeamPlanWithBilling( - metadata.hubId, - plan, - billingDetails, - ); + await this.updateTeamPlanWithBilling(metadata.hubId, plan, billingDetails); if (!isDowngrading) { + const teamIdObject = new ObjectId(metadata.hubId); + const updateTeam = await this.downgradeTeamRepository.findTeamByTeamId(teamIdObject); await this.downgradeService.unRestrictWorkpsaces(updateTeam); } // Update team with new license data From 2e31c99b85ff77a33f8ccba690cd32aaa4ddb2c0 Mon Sep 17 00:00:00 2001 From: "aakash.reddy@techdome.net.in" Date: Tue, 14 Oct 2025 12:38:24 +0530 Subject: [PATCH 10/20] feat: workspace restrcited flag are check before returning the workspaces --- .../repositories/workspace.repository.ts | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/src/modules/workspace/repositories/workspace.repository.ts b/src/modules/workspace/repositories/workspace.repository.ts index a21019615..f28b6bf2c 100644 --- a/src/modules/workspace/repositories/workspace.repository.ts +++ b/src/modules/workspace/repositories/workspace.repository.ts @@ -44,6 +44,13 @@ export class WorkspaceRepository { if (!data) { throw new BadRequestException("Not Found"); } + // Check if workspace is restricted + if (data?.isRestricted === true) { + console.log(`🚫 Access denied: Workspace ${_id} is restricted`); + throw new BadRequestException( + "This workspace is restricted and cannot be accessed.", + ); + } return data; } @@ -61,7 +68,16 @@ export class WorkspaceRepository { "The workspaces with that ids could not be found.", ); } - return workspaces; + // Filter out restricted workspaces + const filteredWorkspaces = workspaces.filter((workspace) => { + // Skip if workspace.isRestricted === true + if (workspace?.isRestricted === true) { + console.log(`🚫 Skipping restricted workspace: ${workspace._id}`); + return false; + } + return true; + }); + return filteredWorkspaces; } async getPublicWorkspace(id: string): Promise> { @@ -87,6 +103,16 @@ export class WorkspaceRepository { const response = await this.db .collection(Collections.WORKSPACE) .findOne({ _id: id }); + // Check if workspace is restricted + if (!response) { + throw new BadRequestException("Workspace not found."); + } + if (response?.isRestricted === true) { + console.log(`Access denied: Workspace ${id} is restricted`); + throw new BadRequestException( + "This workspace is restricted and cannot be accessed.", + ); + } return response; } @@ -97,7 +123,17 @@ export class WorkspaceRepository { .collection(Collections.WORKSPACE) .find({ _id: { $in: IdArray } }) .toArray(); - return response; + if (!response || response.length === 0) { + throw new BadRequestException("No workspaces found."); + } + // Filter out restricted workspaces + const filteredResponse = response.filter((workspace) => { + if (workspace?.isRestricted === true) { + return false; + } + return true; + }); + return filteredResponse; } async updateWorkspaceById( From 7b309953bee07c9f4dcc34e1cb59cbcaa926d01d Mon Sep 17 00:00:00 2001 From: "aakash.reddy@techdome.net.in" Date: Tue, 14 Oct 2025 12:57:43 +0530 Subject: [PATCH 11/20] feat: file name corrected. --- src/modules/billing/billing.module.ts | 2 +- ...wngradeTeam.reposiotry.ts => downgradeTeam.repository.ts} | 0 src/modules/billing/services/downgrade.service.ts | 2 +- src/modules/billing/services/stripe-subscription.service.ts | 5 +++-- 4 files changed, 5 insertions(+), 4 deletions(-) rename src/modules/billing/repositories/{downgradeTeam.reposiotry.ts => downgradeTeam.repository.ts} (100%) diff --git a/src/modules/billing/billing.module.ts b/src/modules/billing/billing.module.ts index e7559ca4b..773582f3a 100644 --- a/src/modules/billing/billing.module.ts +++ b/src/modules/billing/billing.module.ts @@ -22,7 +22,7 @@ import { UserRepository } from "@src/modules/identity/repositories/user.reposito import { PricingService } from "@src/modules/workspace/services/pricing.repository"; import { PricingRepository } from "@src/modules/workspace/repositories/pricing.repository"; import { SalesEmailRepository } from "../workspace/repositories/sales-email.repository"; -import { DownGradeTeamRepository } from "./repositories/downgradeTeam.reposiotry"; +import { DownGradeTeamRepository } from "./repositories/downgradeTeam.repository"; import { DownGradeUserRepository } from "./repositories/downgradeUser.repository"; import { DownGradeWorkspaceRepository } from "./repositories/downgradeWorkspace.repository"; import { DownGradeService } from "./services/downgrade.service"; diff --git a/src/modules/billing/repositories/downgradeTeam.reposiotry.ts b/src/modules/billing/repositories/downgradeTeam.repository.ts similarity index 100% rename from src/modules/billing/repositories/downgradeTeam.reposiotry.ts rename to src/modules/billing/repositories/downgradeTeam.repository.ts diff --git a/src/modules/billing/services/downgrade.service.ts b/src/modules/billing/services/downgrade.service.ts index 720233a10..ccc7aedf6 100644 --- a/src/modules/billing/services/downgrade.service.ts +++ b/src/modules/billing/services/downgrade.service.ts @@ -3,7 +3,7 @@ import { TeamRole } from "@src/modules/common/enum/roles.enum"; import { Team } from "@src/modules/common/models/team.model"; import { DeleteResult, ObjectId, WithId } from "mongodb"; import { DowngradeUserDto } from "../payloads/downgrade-user.payload"; -import { DownGradeTeamRepository } from "../repositories/downgradeTeam.reposiotry"; +import { DownGradeTeamRepository } from "../repositories/downgradeTeam.repository"; import { DownGradeUserRepository } from "../repositories/downgradeUser.repository"; import { LicenseManagementService } from "./license-management.service"; import { DownGradeWorkspaceRepository } from "../repositories/downgradeWorkspace.repository"; diff --git a/src/modules/billing/services/stripe-subscription.service.ts b/src/modules/billing/services/stripe-subscription.service.ts index 2b8c9563d..f5326927b 100644 --- a/src/modules/billing/services/stripe-subscription.service.ts +++ b/src/modules/billing/services/stripe-subscription.service.ts @@ -19,7 +19,7 @@ import { LicensesDto } from "@src/modules/common/models/licenses.model"; import { WorkspaceDto } from "@src/modules/common/models/workspace.model"; import { UserDto } from "@src/modules/common/models/user.model"; import { DownGradeService } from "./downgrade.service"; -import { DownGradeTeamRepository } from "../repositories/downgradeTeam.reposiotry"; +import { DownGradeTeamRepository } from "../repositories/downgradeTeam.repository"; import { ObjectId } from "mongodb"; // Dynamically import Stripe service class @@ -633,7 +633,8 @@ export class StripeSubscriptionService { await this.updateTeamPlanWithBilling(metadata.hubId, plan, billingDetails); if (!isDowngrading) { const teamIdObject = new ObjectId(metadata.hubId); - const updateTeam = await this.downgradeTeamRepository.findTeamByTeamId(teamIdObject); + const updateTeam = + await this.downgradeTeamRepository.findTeamByTeamId(teamIdObject); await this.downgradeService.unRestrictWorkpsaces(updateTeam); } // Update team with new license data From b650f9521aaf9d2faca180e7c8aeca6ce302a008 Mon Sep 17 00:00:00 2001 From: Kishan Gupta Date: Tue, 14 Oct 2025 15:22:24 +0530 Subject: [PATCH 12/20] add the generate-pre-script --- .../instructions/generate-pre-script.ts | 110 ++++++++++++++++++ .../controllers/ai-assistant.controller.ts | 37 ++++++ .../services/ai-assistant.service.ts | 105 ++++++++++++++++- 3 files changed, 248 insertions(+), 4 deletions(-) create mode 100644 src/modules/common/instructions/generate-pre-script.ts diff --git a/src/modules/common/instructions/generate-pre-script.ts b/src/modules/common/instructions/generate-pre-script.ts new file mode 100644 index 000000000..f70cab163 --- /dev/null +++ b/src/modules/common/instructions/generate-pre-script.ts @@ -0,0 +1,110 @@ +export const generatePreScriptInstructions = ` + const expect = (actual: any) => ({ + to: { + equal: (expected: any) => { + + }, + notEqual: (expected: any) => { + }, + exist: () => { + + }, + notExist: () => { + + }, + be: { + a: (type: string) => { + }, + true: () => { + }, + false: () => { + + + }, + within: (min: number, max: number) => { + + }, + lessThan: (expected: number) => { + }, + greaterThan: (expected: number) => { + + }, + empty: () => { + + }, + notEmpty: () => { + + }, + }, + contain: (expected: any) => { + + }, + notContain: (expected: any) => { + + }, + beInList: (list: any[]) => { + + }, + notBeInList: (list: any[]) => { + + }, + have: { + all: { + keys: (...keys: string[]) => { + + }, + }, + }, + }, + }); + + const sp = { + response: { + statusCode: number , + body: { + text: () => { + try { + return string; + } catch { + return {}; + } + }, + json: () => { + try { + return JSON.parse(string); + } catch { + return {}; + } + }, + }, + headers: object, + size: number, + time: number, + }, + test: (name: string, fn: Function) => { + try { + fn(); + } catch (err: any) { + } + }, + xmlToJSON: (xml: string) => { + const json = {}; + // Convert XML to JSON logic here + return json; + }, + expect, + }; + + - create testcases using above syntax, + - ensure all test cases are valid syntactically, + - dont use any other syntax or return any other text outside of the test cases. + - dont use any markdown or code snippet + - dont wrap output in triple backticks or labels like "javascript", "js", etc. + - Output must be ONLY the raw test cases in javascript format. + - if user prompt is not valid then return + - Example format: ' + sp.test("userId is a number", function () { + sp.expect(jsonBody.userId).to.be.a("number"); + }); + ' + `; diff --git a/src/modules/workspace/controllers/ai-assistant.controller.ts b/src/modules/workspace/controllers/ai-assistant.controller.ts index 3bb4770f2..5226fc0af 100644 --- a/src/modules/workspace/controllers/ai-assistant.controller.ts +++ b/src/modules/workspace/controllers/ai-assistant.controller.ts @@ -266,4 +266,41 @@ export class AiAssistantController { ); return res.status(responseData.httpStatusCode).send(responseData); } + + @Post("/generate-pre-script") + @ApiOperation({ + summary: "Generate Pre-Script for API request", + description: + "Generates a pre-script for a specific API request definition.", + }) + @UseGuards(JwtAuthGuard, UserLimitGuard) + @ApiResponse({ + status: 200, + description: "Generated Pre-Script Successfully", + }) + @ApiResponse({ + status: 400, + description: "Failed to generate pre-script.", + }) + @ApiResponse({ + status: 500, + description: "Server failed to generate pre-script.", + }) + async generatePreScriptForRequest( + @Body() content: generateTestCasesDto + @Res() res: FastifyReply, + @Req() request: ExtendedFastifyRequest, + ) { + const user = request.user; + const preScript = await this.aiAssistantService.generatePreScript( + user, + content, + ); + const responseData = new ApiResponseService( + "Generated Pre-Script Successfully", + HttpStatusCode.OK, + preScript, + ); + return res.status(responseData.httpStatusCode).send(responseData); + } } diff --git a/src/modules/workspace/services/ai-assistant.service.ts b/src/modules/workspace/services/ai-assistant.service.ts index a020e4f6b..198b0fe85 100644 --- a/src/modules/workspace/services/ai-assistant.service.ts +++ b/src/modules/workspace/services/ai-assistant.service.ts @@ -84,6 +84,7 @@ import { } from "@src/modules/common/enum/collection.request.enum"; import { fixTestScriptInstructions } from "@src/modules/common/instructions/fix-test-script"; import { generateTestCasesInstructions } from "@src/modules/common/instructions/generate-test-cases"; +import { generatePreScriptInstructions } from "@src/modules/common/instructions/generate-pre-script"; async function initializeGenAI(authKey: string, client?: WebSocket) { const { GoogleGenAI } = await import("@google/genai"); @@ -164,7 +165,7 @@ export class AiAssistantService { try { if (!this.endpoint || !this.apiKey || !this.apiVersion) { console.warn("GPT Client is disabled. Missing configuration values"); - }else{ + } else { this.gptAssistantsClient = this.getGPTClient(); } } catch (e) { @@ -181,10 +182,9 @@ export class AiAssistantService { console.warn( "Deepseek Client is disabled. Missing configuration values", ); - }else{ + } else { this.deepseekClient = this.getDeepSeekClient(); } - } catch (e) { console.error(e); } @@ -2492,7 +2492,7 @@ export class AiAssistantService { ], }, }); - + const output = ( response.body as any ).choices?.[0]?.message?.content?.trim(); @@ -2548,6 +2548,103 @@ export class AiAssistantService { } } + /** + * Generates a pre-script for a request item within a collection. + * @param user - The authenticated user requesting the data. + * @param content - The request pre-script content. + * @returns Generated pre-script as JSON or string. + */ + public async generatePreScript( + user: DecodedUserObject, + content: generateTestCasesDto, // Or create a dedicated DTO if needed + ) { + if (!content.text?.trim()) { + throw new BadRequestException("prompt must be provided."); + } + + // You can move this to a separate instructions file if needed + const generatePreScriptInstructions = ` + You are an assistant specialized in generating pre-scripts for API requests. + Given the API details and requirements, generate ONLY the pre-script code needed to set up the request environment. + STRICT RULES: + - Do NOT include explanations, markdown, comments, or any extra text. + - Output ONLY the raw script/code required for the pre-request setup. + - No text outside of the required output format is allowed. + `; + + try { + const response = await this.deepseekClient + .path("/chat/completions") + .post({ + body: { + model: this.deepseekModel, + messages: [ + { + role: "system", + content: generatePreScriptInstructions, + }, + { + role: "user", + content: `Pre-Script Prompt:\n${content.text}\n\n`, + }, + ], + }, + }); + + const output = ( + response.body as any + ).choices?.[0]?.message?.content?.trim(); + if (!output) { + throw new BadRequestException( + "No pre-script generated from the model.", + ); + } + let parsedOutput: any; + try { + parsedOutput = JSON.parse(output); + } catch { + parsedOutput = output; + } + const body = response.body as any; + const tokens = body?.usage?.total_tokens; + + const eventMessage = { + userId: user._id, + tokenCount: tokens, + model: "deepseek", + }; + + await this.producerService.produce(TOPIC.AI_RESPONSE_GENERATED_TOPIC, { + value: JSON.stringify(eventMessage), + }); + + const activityLog = { + userId: user._id.toString(), + userEmail: user.email, + activity: "generate-pre-script", + model: "deepseek", + tokenConsumed: tokens, + threadId: "null", + }; + + // Send activity log to Kafka topic + await this.producerService.produce(TOPIC.AI_ACTIVITY_LOG_TOPIC, { + value: JSON.stringify(activityLog), + }); + + return { result: parsedOutput }; + } catch (error) { + console.error("Error generating pre-script:", error); + Sentry.withScope((scope) => { + scope.setTag("emailId", user.email); + scope.setTag("errorType", "AI"); + Sentry.captureException(error); + }); + throw new BadRequestException( + error?.message || "Failed to generate pre-script. Please try again.", + ); + } + } /** * Generates test cases for a request item within a collection, based on the specified type. * @param user - The authenticated user requesting the data. From e3a51398d6b05978f37dea9fdedd0ecbe3510e39 Mon Sep 17 00:00:00 2001 From: "aakash.reddy@techdome.net.in" Date: Tue, 14 Oct 2025 17:50:03 +0530 Subject: [PATCH 13/20] feat: auto-downgrade restriction setup --- .../billing/helpers/stripe-webhook.helper.ts | 5 +- .../services/stripe-subscription.service.ts | 4 + .../identity/repositories/team.repository.ts | 97 ++++++++++++------- src/modules/identity/services/team.service.ts | 2 +- 4 files changed, 68 insertions(+), 40 deletions(-) diff --git a/src/modules/billing/helpers/stripe-webhook.helper.ts b/src/modules/billing/helpers/stripe-webhook.helper.ts index 1a12449aa..efed9e815 100644 --- a/src/modules/billing/helpers/stripe-webhook.helper.ts +++ b/src/modules/billing/helpers/stripe-webhook.helper.ts @@ -95,10 +95,13 @@ export class StripeWebhookHelper { * Handle subscription updated webhook event */ private async handleSubscriptionUpdated(event: any): Promise { + // Check for resubscription (subscription reactivated) + const isResubscribed = this.detectResubscription(event); // Only handle for specific status changes, like cancellation await this.stripeSubscriptionService.handleSubscriptionUpdated( event.data.object, event.id, + isResubscribed ); // Get the updated team data @@ -106,8 +109,6 @@ export class StripeWebhookHelper { event.data.object.metadata?.hubId, ); - // Check for resubscription (subscription reactivated) - const isResubscribed = this.detectResubscription(event); if (isResubscribed && teamUpdated) { // Send resubscription email await this.paymentEmailHelper.sendSubscriptionResubscribedEmail( diff --git a/src/modules/billing/services/stripe-subscription.service.ts b/src/modules/billing/services/stripe-subscription.service.ts index f5326927b..630589d25 100644 --- a/src/modules/billing/services/stripe-subscription.service.ts +++ b/src/modules/billing/services/stripe-subscription.service.ts @@ -163,6 +163,7 @@ export class StripeSubscriptionService { async handleSubscriptionUpdated( subscription: any, eventId?: string, + isResubscribed?: boolean, ): Promise { try { // Extract metadata and validate required fields @@ -170,6 +171,9 @@ export class StripeSubscriptionService { subscription.metadata, ["planName", "hubId"], ); + if (isResubscribed) { + await this.downgradeService.removeDowngradeDetails(metadata.hubId); + } if (!isValid) { return; diff --git a/src/modules/identity/repositories/team.repository.ts b/src/modules/identity/repositories/team.repository.ts index 2a7b48e99..49197c7d4 100644 --- a/src/modules/identity/repositories/team.repository.ts +++ b/src/modules/identity/repositories/team.repository.ts @@ -84,10 +84,20 @@ export class TeamRepository { "The Team with that id could not be found.", ); } + const isActive = team?.plan?.active; + if (isActive === false) { + return { + _id: team._id, + name: team.name, + hubUrl: team.hubUrl, + logo: team.logo, + isRestricted: true + } as unknown as WithId; + } return team; } - /** + /** * Fetches teams from database by UUID * @param {string[]} teamIds * @returns {Promise} queried team data @@ -101,7 +111,20 @@ export class TeamRepository { "The teams with that ids could not be found.", ); } - return teams; + const processedTeams = teams.map((team) => { + const isActive = team?.plan?.active; + if (isActive === false) { + return { + _id: team._id, + name: team.name, + hubUrl: team.hubUrl, + logo: team.logo, + isRestricted: true + } as Partial>; + } + return team; + }); + return processedTeams as WithId[]; } /** @@ -185,11 +208,11 @@ export class TeamRepository { const responseData = await this.db .collection(Collections.TEAM) .findOneAndUpdate( - { + { _id: id, $expr: { $lt: [{ $size: "$workspaces" }, planData.limits.workspacesPerHub.value], } , - }, + }, { $push: { workspaces: ws }, }, @@ -255,21 +278,21 @@ export class TeamRepository { const incomingEmails = users.map(u => u.email); const result = await this.db.collection(Collections.TEAM).findOneAndUpdate( - { - _id: teamObjectId, - // Ensure limit not exceeded - $expr: { - $lte: [ - { - $add: [ - { $size: { $ifNull: ["$users", []] } }, - { $size: { $ifNull: ["$invites", []] } }, - { - $size: { - $setDifference: [ - incomingEmails, - { - $concatArrays: [ + { + _id: teamObjectId, + // Ensure limit not exceeded + $expr: { + $lte: [ + { + $add: [ + { $size: { $ifNull: ["$users", []] } }, + { $size: { $ifNull: ["$invites", []] } }, + { + $size: { + $setDifference: [ + incomingEmails, + { + $concatArrays: [ { $map: { input: { $ifNull: ["$users", []] }, as: "u", in: "$$u.email" } }, { $map: { input: { $ifNull: ["$invites", []] }, as: "i", in: "$$i.email" } } ] @@ -278,27 +301,27 @@ export class TeamRepository { } } ] - }, + }, { $add: ["$plan.limits.usersPerHub.value", 1] } ] } - }, - [ - { + }, + [ + { $set: { - invites: { - $setUnion: [ - { $ifNull: ["$invites", []] }, // πŸ‘ˆ fallback to [] - { - $filter: { - input: users, // πŸ‘ˆ inject your payload as a constant - as: "newInvite", - cond: { - $not: { - $in: [ - "$$newInvite.email", - { - $concatArrays: [ + invites: { + $setUnion: [ + { $ifNull: ["$invites", []] }, // πŸ‘ˆ fallback to [] + { + $filter: { + input: users, // πŸ‘ˆ inject your payload as a constant + as: "newInvite", + cond: { + $not: { + $in: [ + "$$newInvite.email", + { + $concatArrays: [ { $map: { input: { $ifNull: ["$users", []] }, as: "u", in: "$$u.email" } }, { $map: { input: { $ifNull: ["$invites", []] }, as: "i", in: "$$i.email" } } ] @@ -314,7 +337,7 @@ export class TeamRepository { } ] , { returnDocument: "after" } - ); + ); return result.value; } diff --git a/src/modules/identity/services/team.service.ts b/src/modules/identity/services/team.service.ts index 75e30b942..a23d85e55 100644 --- a/src/modules/identity/services/team.service.ts +++ b/src/modules/identity/services/team.service.ts @@ -462,7 +462,7 @@ export class TeamService { const teamMap = new Map>(); for (const team of teamDocs) { // Sanitize invites - team.invites?.forEach((invite: Invite) => { + team?.invites?.forEach((invite: Invite) => { delete invite.inviteId; delete invite.isAccepted; delete invite.workspaces; From ccab2cbe3b9dcb4712d27337a5c3f91e546b483c Mon Sep 17 00:00:00 2001 From: Jatin Lodhi Date: Tue, 14 Oct 2025 18:34:05 +0530 Subject: [PATCH 14/20] feat: staging workflow for sparrow-api [] --- .github/workflows/staging.yml | 44 +++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/staging.yml diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml new file mode 100644 index 000000000..f21971a35 --- /dev/null +++ b/.github/workflows/staging.yml @@ -0,0 +1,44 @@ +name: Staging +on: + push: + branches: + - releases/infra + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + + - uses: Azure/docker-login@v1 + with: + login-server: sparrowprod.azurecr.io + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + - run: | + docker build . -t sparrowprod.azurecr.io/sparrow-api:${{ github.run_number }} --build-arg GITHUB_TOKEN=${{ secrets.SPARROW_GITHUB_TOKEN }} + docker push sparrowprod.azurecr.io/sparrow-api:${{ github.run_number }} + deploy: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - uses: richardrigutins/replace-in-files@v1 + with: + files: "./deploymentManifests/deployment.yml" + search-text: '_BUILD__ID_' + replacement-text: '${{ github.run_number }}' + + - uses: azure/setup-kubectl@v2.0 + + - uses: Azure/k8s-set-context@v2 + with: + kubeconfig: ${{ secrets.KUBE_CONFIG }} + + - uses: Azure/k8s-deploy@v4 + with: + action: deploy + namespace: sparrow-staging + manifests: | + ./deploymentManifests/deployment.yml From 98e3b233948cebf2a08e7c900810e800f7c4a597 Mon Sep 17 00:00:00 2001 From: Jatin Lodhi Date: Wed, 15 Oct 2025 15:22:30 +0530 Subject: [PATCH 15/20] feat: add stg workflow and mac and linux version env[] --- .github/workflows/staging.yml | 2 +- deploymentManifests/deployment.yml | 10 ++++++++++ deploymentManifests/release-v2.yml | 10 ++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index f21971a35..ca715fe42 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -2,7 +2,7 @@ name: Staging on: push: branches: - - releases/infra + - release/2.34.0 jobs: build: diff --git a/deploymentManifests/deployment.yml b/deploymentManifests/deployment.yml index f5e07b483..6fbdf165a 100644 --- a/deploymentManifests/deployment.yml +++ b/deploymentManifests/deployment.yml @@ -411,6 +411,16 @@ spec: secretKeyRef: name: sparrow-api-secret key: SPARROW_PROXY_BASE_URL + - name: APP_MAC_VERSION + valueFrom: + secretKeyRef: + name: sparrow-api-secret + key: APP_MAC_VERSION + - name: APP_LINUX_VERSION + valueFrom: + secretKeyRef: + name: sparrow-api-secret + key: APP_LINUX_VERSION --- apiVersion: v1 diff --git a/deploymentManifests/release-v2.yml b/deploymentManifests/release-v2.yml index b3380c24b..7e9905f0d 100644 --- a/deploymentManifests/release-v2.yml +++ b/deploymentManifests/release-v2.yml @@ -380,6 +380,16 @@ spec: secretKeyRef: name: release-api-v2-secret key: SPARROW_PROXY_BASE_URL + - name: APP_MAC_VERSION + valueFrom: + secretKeyRef: + name: release-api-v2-secret + key: APP_MAC_VERSION + - name: APP_LINUX_VERSION + valueFrom: + secretKeyRef: + name: release-api-v2-secret + key: APP_LINUX_VERSION --- From e917ad8aae262a739c541e27f42837e9370e45c0 Mon Sep 17 00:00:00 2001 From: Jatin Lodhi Date: Wed, 15 Oct 2025 15:24:18 +0530 Subject: [PATCH 16/20] feat: add stg workflow [] --- deploymentManifests/deployment.yml | 11 +---------- deploymentManifests/release-v2.yml | 11 +---------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/deploymentManifests/deployment.yml b/deploymentManifests/deployment.yml index 6fbdf165a..ec1754561 100644 --- a/deploymentManifests/deployment.yml +++ b/deploymentManifests/deployment.yml @@ -411,16 +411,7 @@ spec: secretKeyRef: name: sparrow-api-secret key: SPARROW_PROXY_BASE_URL - - name: APP_MAC_VERSION - valueFrom: - secretKeyRef: - name: sparrow-api-secret - key: APP_MAC_VERSION - - name: APP_LINUX_VERSION - valueFrom: - secretKeyRef: - name: sparrow-api-secret - key: APP_LINUX_VERSION + --- apiVersion: v1 diff --git a/deploymentManifests/release-v2.yml b/deploymentManifests/release-v2.yml index 7e9905f0d..05394c158 100644 --- a/deploymentManifests/release-v2.yml +++ b/deploymentManifests/release-v2.yml @@ -380,16 +380,7 @@ spec: secretKeyRef: name: release-api-v2-secret key: SPARROW_PROXY_BASE_URL - - name: APP_MAC_VERSION - valueFrom: - secretKeyRef: - name: release-api-v2-secret - key: APP_MAC_VERSION - - name: APP_LINUX_VERSION - valueFrom: - secretKeyRef: - name: release-api-v2-secret - key: APP_LINUX_VERSION + --- From 8095ae9fe05fa6c85e9b551b146637356d9ca099 Mon Sep 17 00:00:00 2001 From: "aakash.reddy@techdome.net.in" Date: Thu, 16 Oct 2025 00:01:27 +0530 Subject: [PATCH 17/20] feat: autodown grade with email template --- package.json | 1 + pnpm-lock.yaml | 448 +++++++++++++++++- src/modules/billing/billing.module.ts | 3 + .../billing/helpers/payment-email.helper.ts | 62 +++ .../payloads/downgrade-user.payload.ts | 13 + .../repositories/downgradeUser.repository.ts | 18 +- .../downgradeWorkspace.repository.ts | 51 +- .../billing/services/downgrade.service.ts | 13 + .../billing/services/excel-email.service.ts | 157 ++++++ .../billing/services/payment-email.service.ts | 100 ++++ .../services/stripe-subscription.service.ts | 136 +++++- .../common/services/blobStorage.service.ts | 74 ++- .../hubDowngradeRemoveUserEmail.handlebars | 143 ++++++ .../views/hubDowngradedEmail.handlebars | 201 ++++++++ 14 files changed, 1390 insertions(+), 30 deletions(-) create mode 100644 src/modules/billing/services/excel-email.service.ts create mode 100644 src/modules/views/hubDowngradeRemoveUserEmail.handlebars create mode 100644 src/modules/views/hubDowngradedEmail.handlebars diff --git a/package.json b/package.json index f5acbe065..724b41a69 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "crypto-js": "^4.2.0", "curlconverter": "^4.9.0", "dotenv": "16.0.1", + "exceljs": "^4.4.0", "fastify": "4.28.1", "form-data": "^4.0.1", "gravatar": "1.8.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3de31bf90..61249ec40 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -140,6 +140,9 @@ importers: dotenv: specifier: 16.0.1 version: 16.0.1 + exceljs: + specifier: ^4.4.0 + version: 4.4.0 fastify: specifier: 4.28.1 version: 4.28.1 @@ -230,7 +233,7 @@ importers: optionalDependencies: '@sparrowapp-dev/stripe-billing': specifier: ^1.3.1 - version: 1.3.1(@fastify/static@6.12.0)(@nestjs/common@10.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.13)(rxjs@7.5.6))(@nestjs/core@10.4.20(@nestjs/common@10.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.13)(rxjs@7.5.6))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@10.4.20)(reflect-metadata@0.1.13)(rxjs@7.5.6))(@types/node@16.11.56) + version: 1.3.1(@fastify/static@6.12.0)(@nestjs/common@10.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.13)(rxjs@7.5.6))(@nestjs/core@10.4.20)(@types/node@16.11.56) devDependencies: '@compodoc/compodoc': specifier: 1.1.19 @@ -1259,6 +1262,12 @@ packages: resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fast-csv/format@4.3.5': + resolution: {integrity: sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A==} + + '@fast-csv/parse@4.3.6': + resolution: {integrity: sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA==} + '@fastify/accept-negotiator@1.1.0': resolution: {integrity: sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==} engines: {node: '>=14'} @@ -2556,6 +2565,9 @@ packages: '@types/mysql@2.15.26': resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==} + '@types/node@14.18.63': + resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==} + '@types/node@16.11.56': resolution: {integrity: sha512-aFcUkv7EddxxOa/9f74DINReQ/celqH8DiB3fRYgVDM2Xm5QJL8sl80QKuAnGvwAsMn+H3IFA6WCrQh1CY7m1A==} @@ -2839,6 +2851,18 @@ packages: resolution: {integrity: sha512-WzeSPqjut5NiQfZ68+k7i//bWzLdTZvN6BvwiiTaytlsmMVZrfIGozwLVPPtiH48faFVDm5LbZjmol4QUcn3Mg==} engines: {node: '>=8.0.0'} + archiver-utils@2.1.0: + resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} + engines: {node: '>= 6'} + + archiver-utils@3.0.4: + resolution: {integrity: sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==} + engines: {node: '>= 10'} + + archiver@5.3.2: + resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==} + engines: {node: '>= 10'} + arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -2876,6 +2900,9 @@ packages: async-retry@1.3.3: resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -2960,6 +2987,10 @@ packages: bcryptjs@2.4.3: resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==} + big-integer@1.6.52: + resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} + engines: {node: '>=0.6'} + bignumber.js@9.3.1: resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} @@ -2967,12 +2998,18 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + binary@0.3.0: + resolution: {integrity: sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==} + bintrees@1.0.2: resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==} bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + bluebird@3.4.7: + resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} + blueimp-md5@2.19.0: resolution: {integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==} @@ -3015,18 +3052,29 @@ packages: resolution: {integrity: sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==} engines: {node: '>=14.20.1'} + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer-indexof-polyfill@1.0.2: + resolution: {integrity: sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==} + engines: {node: '>=0.10'} + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + buffers@0.1.1: + resolution: {integrity: sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==} + engines: {node: '>=0.2.0'} + bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} @@ -3076,6 +3124,9 @@ packages: resolution: {integrity: sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==} engines: {node: '>=4'} + chainsaw@0.1.0: + resolution: {integrity: sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -3195,6 +3246,10 @@ packages: component-emitter@1.3.1: resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + compress-commons@4.1.2: + resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==} + engines: {node: '>= 10'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -3241,6 +3296,9 @@ packages: core-js-compat@3.45.1: resolution: {integrity: sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==} + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.5: resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} engines: {node: '>= 0.10'} @@ -3249,6 +3307,15 @@ packages: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + crc32-stream@4.0.3: + resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==} + engines: {node: '>= 10'} + create-jest@29.7.0: resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -3294,6 +3361,9 @@ packages: dateformat@4.6.3: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + dayjs@1.11.18: + resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==} + debug@2.6.9: resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} peerDependencies: @@ -3476,6 +3546,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + duplexer2@0.1.4: + resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} + duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -3668,6 +3741,10 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + exceljs@4.4.0: + resolution: {integrity: sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg==} + engines: {node: '>=8.3.0'} + execa@4.1.0: resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} engines: {node: '>=10'} @@ -3709,6 +3786,10 @@ packages: fast-copy@3.0.2: resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} + fast-csv@4.3.6: + resolution: {integrity: sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw==} + engines: {node: '>=10.0.0'} + fast-decode-uri-component@1.0.1: resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} @@ -3851,6 +3932,9 @@ packages: from@0.1.7: resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-extra@10.1.0: resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} engines: {node: '>=12'} @@ -3867,6 +3951,11 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + fstream@1.0.12: + resolution: {integrity: sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==} + engines: {node: '>=0.6'} + deprecated: This package is no longer supported. + function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} @@ -4114,6 +4203,9 @@ packages: engines: {node: '>=16.x'} hasBin: true + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -4326,6 +4418,9 @@ packages: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@2.0.5: resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} @@ -4565,6 +4660,9 @@ packages: jssha@3.3.1: resolution: {integrity: sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==} + jszip@3.10.1: + resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + jwa@1.4.2: resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} @@ -4584,6 +4682,10 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} + lazystream@1.0.1: + resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} + engines: {node: '>= 0.6.3'} + leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -4595,6 +4697,9 @@ packages: libphonenumber-js@1.12.15: resolution: {integrity: sha512-TMDCtIhWUDHh91wRC+wFuGlIzKdPzaTUHHVrIZ3vPUEoNaXFLrsIQ1ZpAeZeXApIF6rvDksMTvjrIQlLKaYxqQ==} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + light-my-request@5.14.0: resolution: {integrity: sha512-aORPWntbpH5esaYpGOOmri0OHDOe3wC5M2MQxZ9dvMLZm6DnaAn0kJlcbU9hwsQgLzmZyReKwFwwPkR+nHu5kA==} @@ -4613,6 +4718,9 @@ packages: engines: {node: ^16.14.0 || >=18.0.0} hasBin: true + listenercount@1.0.1: + resolution: {integrity: sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==} + listr2@6.6.1: resolution: {integrity: sha512-+rAXGHh0fkEWdXBmX+L6mmfmXmXvDGEKzkjxO+8mP3+nI/r/CWznVBvsibXdxda9Zz0OW2e2ikphN3OwCT/jSg==} engines: {node: '>=16.0.0'} @@ -4636,19 +4744,44 @@ packages: lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.difference@4.5.0: + resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} + + lodash.escaperegexp@4.1.2: + resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} + + lodash.flatten@4.4.0: + resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} + lodash.get@4.4.2: resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. + lodash.groupby@4.6.0: + resolution: {integrity: sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==} + lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} lodash.isboolean@3.0.3: resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + + lodash.isfunction@3.0.9: + resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==} + lodash.isinteger@4.0.4: resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + lodash.isnil@4.0.0: + resolution: {integrity: sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==} + lodash.isnumber@3.0.3: resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} @@ -4658,6 +4791,9 @@ packages: lodash.isstring@4.0.1: resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.isundefined@3.0.1: + resolution: {integrity: sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA==} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} @@ -4667,6 +4803,12 @@ packages: lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.union@4.6.0: + resolution: {integrity: sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==} + + lodash.uniq@4.5.0: + resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -5105,6 +5247,9 @@ packages: pako@0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -5311,6 +5456,9 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process-warning@3.0.0: resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==} @@ -5394,6 +5542,9 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -5402,6 +5553,9 @@ packages: resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + readdir-glob@1.1.3: + resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} + readdirp@3.6.0: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} @@ -5507,6 +5661,11 @@ packages: rhea@3.0.4: resolution: {integrity: sha512-n3kw8syCdrsfJ72w3rohpoHHlmv/RZZEP9VY5BVjjo0sEGIt4YSKypBgaiA+OUSgJAzLjOECYecsclG5xbYtZw==} + rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + rimraf@3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -5560,6 +5719,10 @@ packages: sax@1.4.1: resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + saxes@5.0.1: + resolution: {integrity: sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==} + engines: {node: '>=10'} + secure-json-parse@2.7.0: resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} @@ -5606,6 +5769,9 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + setprototypeof@1.1.0: resolution: {integrity: sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==} @@ -5777,6 +5943,9 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -5853,6 +6022,10 @@ packages: resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + tdigest@0.1.2: resolution: {integrity: sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==} @@ -5876,6 +6049,10 @@ packages: resolution: {integrity: sha512-hkcz3FjNJfKXjV4mjQ1OrXSLAehg8Hw+cEZclOVT+5c/cWQWImQ9wolzTjth+dmmDe++p3bme3fTxz6Q4Etsqw==} engines: {node: '>=12'} + tmp@0.2.5: + resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} + engines: {node: '>=14.14'} + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -5902,6 +6079,9 @@ packages: resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} engines: {node: '>=12'} + traverse@0.3.9: + resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==} + traverse@0.6.11: resolution: {integrity: sha512-vxXDZg8/+p3gblxB6BhhG5yWVn1kGRlaL8O78UDXc3wRnPizB5g83dcvWV1jpDMIPnjZjOFuxlMmE82XJ4407w==} engines: {node: '>= 0.4'} @@ -6095,6 +6275,9 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + unzipper@0.10.14: + resolution: {integrity: sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==} + update-browserslist-db@1.1.3: resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} hasBin: true @@ -6278,6 +6461,9 @@ packages: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} engines: {node: '>=18'} + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xmldoc@2.0.2: resolution: {integrity: sha512-UiRwoSStEXS3R+YE8OqYv3jebza8cBBAI2y8g3B15XFkn3SbEOyyLnmPHjLBPZANrPJKEzxxB7A3XwcLikQVlQ==} engines: {node: '>=12.0.0'} @@ -6336,6 +6522,10 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zip-stream@4.1.1: + resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} + engines: {node: '>= 10'} + snapshots: '@aduh95/viz.js@3.4.0': {} @@ -7892,6 +8082,25 @@ snapshots: '@eslint/core': 0.15.2 levn: 0.4.1 + '@fast-csv/format@4.3.5': + dependencies: + '@types/node': 14.18.63 + lodash.escaperegexp: 4.1.2 + lodash.isboolean: 3.0.3 + lodash.isequal: 4.5.0 + lodash.isfunction: 3.0.9 + lodash.isnil: 4.0.0 + + '@fast-csv/parse@4.3.6': + dependencies: + '@types/node': 14.18.63 + lodash.escaperegexp: 4.1.2 + lodash.groupby: 4.6.0 + lodash.isfunction: 3.0.9 + lodash.isnil: 4.0.0 + lodash.isundefined: 3.0.1 + lodash.uniq: 4.5.0 + '@fastify/accept-negotiator@1.1.0': {} '@fastify/ajv-compiler@3.6.0': @@ -8388,23 +8597,6 @@ snapshots: '@nestjs/core': 10.4.20(@nestjs/common@10.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.13)(rxjs@7.5.6))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@10.4.20)(reflect-metadata@0.1.13)(rxjs@7.5.6) cron: 4.3.3 - '@nestjs/swagger@7.4.2(@fastify/static@6.12.0)(@nestjs/common@10.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.13)(rxjs@7.5.6))(@nestjs/core@10.4.20(@nestjs/common@10.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.13)(rxjs@7.5.6))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@10.4.20)(reflect-metadata@0.1.13)(rxjs@7.5.6))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.13)': - dependencies: - '@microsoft/tsdoc': 0.15.1 - '@nestjs/common': 10.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.13)(rxjs@7.5.6) - '@nestjs/core': 10.4.20(@nestjs/common@10.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.13)(rxjs@7.5.6))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@10.4.20)(reflect-metadata@0.1.13)(rxjs@7.5.6) - '@nestjs/mapped-types': 2.0.5(@nestjs/common@10.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.13)(rxjs@7.5.6))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.13) - js-yaml: 4.1.0 - lodash: 4.17.21 - path-to-regexp: 3.3.0 - reflect-metadata: 0.1.13 - swagger-ui-dist: 5.17.14 - optionalDependencies: - '@fastify/static': 6.12.0 - class-transformer: 0.5.1 - class-validator: 0.14.2 - optional: true - '@nestjs/swagger@7.4.2(@fastify/static@6.12.0)(@nestjs/common@10.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.13)(rxjs@7.5.6))(@nestjs/core@10.4.20)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.13)': dependencies: '@microsoft/tsdoc': 0.15.1 @@ -9531,13 +9723,13 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} - '@sparrowapp-dev/stripe-billing@1.3.1(@fastify/static@6.12.0)(@nestjs/common@10.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.13)(rxjs@7.5.6))(@nestjs/core@10.4.20(@nestjs/common@10.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.13)(rxjs@7.5.6))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@10.4.20)(reflect-metadata@0.1.13)(rxjs@7.5.6))(@types/node@16.11.56)': + '@sparrowapp-dev/stripe-billing@1.3.1(@fastify/static@6.12.0)(@nestjs/common@10.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.13)(rxjs@7.5.6))(@nestjs/core@10.4.20)(@types/node@16.11.56)': dependencies: '@nestjs/common': 10.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.13)(rxjs@7.5.6) '@nestjs/config': 3.3.0(@nestjs/common@10.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.13)(rxjs@7.5.6))(rxjs@7.8.2) '@nestjs/core': 10.4.20(@nestjs/common@10.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.13)(rxjs@7.5.6))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@10.4.20)(reflect-metadata@0.1.13)(rxjs@7.5.6) '@nestjs/platform-express': 10.4.20(@nestjs/common@10.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.13)(rxjs@7.5.6))(@nestjs/core@10.4.20) - '@nestjs/swagger': 7.4.2(@fastify/static@6.12.0)(@nestjs/common@10.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.13)(rxjs@7.5.6))(@nestjs/core@10.4.20(@nestjs/common@10.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.13)(rxjs@7.5.6))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@10.4.20)(reflect-metadata@0.1.13)(rxjs@7.5.6))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.13) + '@nestjs/swagger': 7.4.2(@fastify/static@6.12.0)(@nestjs/common@10.1.3(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.13)(rxjs@7.5.6))(@nestjs/core@10.4.20)(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.1.13) class-transformer: 0.5.1 class-validator: 0.14.2 reflect-metadata: 0.1.13 @@ -9693,6 +9885,8 @@ snapshots: dependencies: '@types/node': 16.11.56 + '@types/node@14.18.63': {} + '@types/node@16.11.56': {} '@types/nodemailer-express-handlebars@4.0.5': @@ -10054,6 +10248,42 @@ snapshots: transitivePeerDependencies: - supports-color + archiver-utils@2.1.0: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 2.3.8 + + archiver-utils@3.0.4: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + + archiver@5.3.2: + dependencies: + archiver-utils: 2.1.0 + async: 3.2.6 + buffer-crc32: 0.2.13 + readable-stream: 3.6.2 + readdir-glob: 1.1.3 + tar-stream: 2.2.0 + zip-stream: 4.1.1 + arg@4.1.3: {} argparse@1.0.10: @@ -10092,6 +10322,8 @@ snapshots: dependencies: retry: 0.13.1 + async@3.2.6: {} + asynckit@0.4.0: {} atomic-sleep@1.0.0: {} @@ -10210,10 +10442,17 @@ snapshots: bcryptjs@2.4.3: {} + big-integer@1.6.52: {} + bignumber.js@9.3.1: {} binary-extensions@2.3.0: {} + binary@0.3.0: + dependencies: + buffers: 0.1.1 + chainsaw: 0.1.0 + bintrees@1.0.2: {} bl@4.1.0: @@ -10222,6 +10461,8 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + bluebird@3.4.7: {} + blueimp-md5@2.19.0: {} body-parser@1.20.3: @@ -10280,10 +10521,14 @@ snapshots: bson@5.5.1: {} + buffer-crc32@0.2.13: {} + buffer-equal-constant-time@1.0.1: {} buffer-from@1.1.2: {} + buffer-indexof-polyfill@1.0.2: {} + buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -10294,6 +10539,8 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + buffers@0.1.1: {} + bundle-name@4.1.0: dependencies: run-applescript: 7.0.0 @@ -10345,6 +10592,10 @@ snapshots: pathval: 1.1.1 type-detect: 4.1.0 + chainsaw@0.1.0: + dependencies: + traverse: 0.3.9 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -10469,6 +10720,13 @@ snapshots: component-emitter@1.3.1: {} + compress-commons@4.1.2: + dependencies: + buffer-crc32: 0.2.13 + crc32-stream: 4.0.3 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + concat-map@0.0.1: {} concat-stream@2.0.0: @@ -10515,6 +10773,8 @@ snapshots: dependencies: browserslist: 4.25.4 + core-util-is@1.0.3: {} + cors@2.8.5: dependencies: object-assign: 4.1.1 @@ -10528,6 +10788,13 @@ snapshots: path-type: 4.0.0 yaml: 1.10.2 + crc-32@1.2.2: {} + + crc32-stream@4.0.3: + dependencies: + crc-32: 1.2.2 + readable-stream: 3.6.2 + create-jest@29.7.0(@types/node@16.11.56)(ts-node@10.9.1(@types/node@16.11.56)(typescript@5.7.2)): dependencies: '@jest/types': 29.6.3 @@ -10597,6 +10864,8 @@ snapshots: dateformat@4.6.3: {} + dayjs@1.11.18: {} + debug@2.6.9: dependencies: ms: 2.0.0 @@ -10739,6 +11008,10 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + duplexer2@0.1.4: + dependencies: + readable-stream: 2.3.8 + duplexer@0.1.2: {} eastasianwidth@0.2.0: {} @@ -10998,6 +11271,18 @@ snapshots: events@3.3.0: {} + exceljs@4.4.0: + dependencies: + archiver: 5.3.2 + dayjs: 1.11.18 + fast-csv: 4.3.6 + jszip: 3.10.1 + readable-stream: 3.6.2 + saxes: 5.0.1 + tmp: 0.2.5 + unzipper: 0.10.14 + uuid: 8.3.2 + execa@4.1.0: dependencies: cross-spawn: 7.0.6 @@ -11097,6 +11382,11 @@ snapshots: fast-copy@3.0.2: {} + fast-csv@4.3.6: + dependencies: + '@fast-csv/format': 4.3.5 + '@fast-csv/parse': 4.3.6 + fast-decode-uri-component@1.0.1: {} fast-deep-equal@3.1.3: {} @@ -11271,6 +11561,8 @@ snapshots: from@0.1.7: {} + fs-constants@1.0.0: {} + fs-extra@10.1.0: dependencies: graceful-fs: 4.2.11 @@ -11288,6 +11580,13 @@ snapshots: fsevents@2.3.3: optional: true + fstream@1.0.12: + dependencies: + graceful-fs: 4.2.11 + inherits: 2.0.4 + mkdirp: 0.5.6 + rimraf: 2.7.1 + function-bind@1.1.2: {} function.prototype.name@1.1.8: @@ -11569,6 +11868,8 @@ snapshots: image-size@2.0.2: {} + immediate@3.0.6: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -11761,6 +12062,8 @@ snapshots: dependencies: is-inside-container: 1.0.0 + isarray@1.0.0: {} + isarray@2.0.5: {} isexe@2.0.0: {} @@ -12189,6 +12492,13 @@ snapshots: jssha@3.3.1: {} + jszip@3.10.1: + dependencies: + lie: 3.3.0 + pako: 1.0.11 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + jwa@1.4.2: dependencies: buffer-equal-constant-time: 1.0.1 @@ -12217,6 +12527,10 @@ snapshots: kleur@3.0.3: {} + lazystream@1.0.1: + dependencies: + readable-stream: 2.3.8 + leven@3.1.0: {} levn@0.4.1: @@ -12226,6 +12540,10 @@ snapshots: libphonenumber-js@1.12.15: {} + lie@3.3.0: + dependencies: + immediate: 3.0.6 + light-my-request@5.14.0: dependencies: cookie: 0.7.2 @@ -12258,6 +12576,8 @@ snapshots: - enquirer - supports-color + listenercount@1.0.1: {} + listr2@6.6.1: dependencies: cli-truncate: 3.1.0 @@ -12279,26 +12599,48 @@ snapshots: lodash.debounce@4.0.8: {} + lodash.defaults@4.2.0: {} + + lodash.difference@4.5.0: {} + + lodash.escaperegexp@4.1.2: {} + + lodash.flatten@4.4.0: {} + lodash.get@4.4.2: {} + lodash.groupby@4.6.0: {} + lodash.includes@4.3.0: {} lodash.isboolean@3.0.3: {} + lodash.isequal@4.5.0: {} + + lodash.isfunction@3.0.9: {} + lodash.isinteger@4.0.4: {} + lodash.isnil@4.0.0: {} + lodash.isnumber@3.0.3: {} lodash.isplainobject@4.0.6: {} lodash.isstring@4.0.1: {} + lodash.isundefined@3.0.1: {} + lodash.memoize@4.1.2: {} lodash.merge@4.6.2: {} lodash.once@4.1.1: {} + lodash.union@4.6.0: {} + + lodash.uniq@4.5.0: {} + lodash@4.17.21: {} log-symbols@4.1.0: @@ -12440,7 +12782,6 @@ snapshots: mkdirp@0.5.6: dependencies: minimist: 1.2.8 - optional: true mkdirp@1.0.4: {} @@ -12682,6 +13023,8 @@ snapshots: pako@0.2.9: {} + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -12907,6 +13250,8 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 + process-nextick-args@2.0.1: {} + process-warning@3.0.0: {} process-warning@4.0.1: {} @@ -12989,6 +13334,16 @@ snapshots: react-is@18.3.1: {} + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -13003,6 +13358,10 @@ snapshots: process: 0.11.10 string_decoder: 1.3.0 + readdir-glob@1.1.3: + dependencies: + minimatch: 5.1.6 + readdirp@3.6.0: dependencies: picomatch: 2.3.1 @@ -13116,6 +13475,10 @@ snapshots: transitivePeerDependencies: - supports-color + rimraf@2.7.1: + dependencies: + glob: 7.2.3 + rimraf@3.0.2: dependencies: glob: 7.2.3 @@ -13172,6 +13535,10 @@ snapshots: sax@1.4.1: {} + saxes@5.0.1: + dependencies: + xmlchars: 2.2.0 + secure-json-parse@2.7.0: {} semver@6.3.1: {} @@ -13261,6 +13628,8 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 + setimmediate@1.0.5: {} + setprototypeof@1.1.0: {} setprototypeof@1.2.0: {} @@ -13465,6 +13834,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -13538,6 +13911,14 @@ snapshots: dependencies: '@pkgr/core': 0.2.9 + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + tdigest@0.1.2: dependencies: bintrees: 1.0.2 @@ -13562,6 +13943,8 @@ snapshots: tiny-lru@11.4.5: {} + tmp@0.2.5: {} + tmpl@1.0.5: {} to-regex-range@5.0.1: @@ -13580,6 +13963,8 @@ snapshots: dependencies: punycode: 2.3.1 + traverse@0.3.9: {} + traverse@0.6.11: dependencies: gopd: 1.2.0 @@ -13784,6 +14169,19 @@ snapshots: unpipe@1.0.0: {} + unzipper@0.10.14: + dependencies: + big-integer: 1.6.52 + binary: 0.3.0 + bluebird: 3.4.7 + buffer-indexof-polyfill: 1.0.2 + duplexer2: 0.1.4 + fstream: 1.0.12 + graceful-fs: 4.2.11 + listenercount: 1.0.1 + readable-stream: 2.3.8 + setimmediate: 1.0.5 + update-browserslist-db@1.1.3(browserslist@4.25.4): dependencies: browserslist: 4.25.4 @@ -13958,6 +14356,8 @@ snapshots: dependencies: is-wsl: 3.1.0 + xmlchars@2.2.0: {} + xmldoc@2.0.2: dependencies: sax: 1.4.1 @@ -14015,3 +14415,9 @@ snapshots: yn@3.1.1: {} yocto-queue@0.1.0: {} + + zip-stream@4.1.1: + dependencies: + archiver-utils: 3.0.4 + compress-commons: 4.1.2 + readable-stream: 3.6.2 diff --git a/src/modules/billing/billing.module.ts b/src/modules/billing/billing.module.ts index 773582f3a..9e7ee668f 100644 --- a/src/modules/billing/billing.module.ts +++ b/src/modules/billing/billing.module.ts @@ -26,6 +26,7 @@ import { DownGradeTeamRepository } from "./repositories/downgradeTeam.repository import { DownGradeUserRepository } from "./repositories/downgradeUser.repository"; import { DownGradeWorkspaceRepository } from "./repositories/downgradeWorkspace.repository"; import { DownGradeService } from "./services/downgrade.service"; +import { ExcelEmailService } from "./services/excel-email.service"; // Try to import the Stripe module, but don't crash if it's not available let StripeModule: any; @@ -56,6 +57,7 @@ export class BillingModule { DownGradeUserRepository, DownGradeWorkspaceRepository, DownGradeService, + ExcelEmailService, BillingAuditService, PromoCodeService, PricingService, @@ -85,6 +87,7 @@ export class BillingModule { DownGradeUserRepository, DownGradeWorkspaceRepository, DownGradeService, + ExcelEmailService, PricingService, PaymentEmailService, PaymentEmailHelper, diff --git a/src/modules/billing/helpers/payment-email.helper.ts b/src/modules/billing/helpers/payment-email.helper.ts index 4c4d08776..741921a8a 100644 --- a/src/modules/billing/helpers/payment-email.helper.ts +++ b/src/modules/billing/helpers/payment-email.helper.ts @@ -6,6 +6,10 @@ import { } from "../services/payment-email.service"; import { StripeCustomerService } from "../services/stripe-customer.service"; import { PlanName } from "@src/modules/common/enum/plan.enum"; +import { + UserExcelDto, + WorkspaceExcelDto, +} from "../payloads/downgrade-user.payload"; // Dynamically import payment methods service let PaymentMethodsService: any; @@ -187,6 +191,64 @@ export class PaymentEmailHelper { } } + /** + * Send plan downgraded emails to existing users in Hub. + */ + async sendHubDowngradedEmail( + team: any, + startDate: Date, + previousPlan: string, + newPlan: string, + sendEmails: string[], + workspaces: WorkspaceExcelDto[], + users: UserExcelDto[], + ): Promise { + try { + const emailData = await this.buildPlanDowngradeEmailData( + team, + startDate, + previousPlan, + newPlan, + ); + if (!emailData) return; + const updateEmailData = { ...emailData, sendEmails, workspaces, users }; + await this.paymentEmailService.sendPaymentEmail( + PaymentEmailType.HUB_DOWNGRADED, + updateEmailData, + ); + } catch (error) { + console.error("Error sending plan downgraded email:", error); + } + } + + /** + * Send plan downgraded emails to Removed users in Hub. + */ + async sendHubDowngradeRemoveUserEmail( + team: any, + startDate: Date, + previousPlan: string, + newPlan: string, + sendEmails: string[], + ): Promise { + try { + const emailData = await this.buildPlanDowngradeEmailData( + team, + startDate, + previousPlan, + newPlan, + ); + if (!emailData) return; + const updateEmailData = { ...emailData, sendEmails }; + await this.paymentEmailService.sendPaymentEmail( + PaymentEmailType.HUB_DOWNGRADED_REMOVE_USER, + updateEmailData, + ); + } catch (error) { + console.error("Error sending plan downgraded email:", error); + } + } + /** * Send upcoming payment email using customer's default payment method email */ diff --git a/src/modules/billing/payloads/downgrade-user.payload.ts b/src/modules/billing/payloads/downgrade-user.payload.ts index b59cf5c4c..36347e07e 100644 --- a/src/modules/billing/payloads/downgrade-user.payload.ts +++ b/src/modules/billing/payloads/downgrade-user.payload.ts @@ -112,3 +112,16 @@ export class DownGradeUserTourGuideDto extends DownGradeUserGenerateVariableDto @Type(() => TourGuideDto) tourGuide?: TourGuideDto; } + +export interface WorkspaceExcelDto { + name: string; + created_at: string; + collections: number; + testflow: number; +} + +export interface UserExcelDto { + name: string; + email: string; +} + diff --git a/src/modules/billing/repositories/downgradeUser.repository.ts b/src/modules/billing/repositories/downgradeUser.repository.ts index fb640ffb5..ca19dea3b 100644 --- a/src/modules/billing/repositories/downgradeUser.repository.ts +++ b/src/modules/billing/repositories/downgradeUser.repository.ts @@ -1,4 +1,4 @@ -import { Injectable, Inject } from "@nestjs/common"; +import { Injectable, Inject, BadRequestException } from "@nestjs/common"; import { Db } from "mongodb"; import { ObjectId, WithId } from "mongodb"; import { User } from "@src/modules/common/models/user.model"; @@ -38,4 +38,20 @@ export class DownGradeUserRepository { .toArray(); return response; } + + async findUsersByStringIds(ids: string[]): Promise[]> { + try { + const objectIds = ids.map((id) => new ObjectId(id)); + const users = await this.db + .collection(Collections.USER) + .find({ _id: { $in: objectIds } }) + .toArray(); + if (!users || users.length === 0) { + return; + } + return users; + } catch (error) { + console.error("Error fetching users by string IDs:", error); + } + } } diff --git a/src/modules/billing/repositories/downgradeWorkspace.repository.ts b/src/modules/billing/repositories/downgradeWorkspace.repository.ts index 952b93b4f..04235ed7e 100644 --- a/src/modules/billing/repositories/downgradeWorkspace.repository.ts +++ b/src/modules/billing/repositories/downgradeWorkspace.repository.ts @@ -1,6 +1,6 @@ import { Injectable, Inject, BadRequestException } from "@nestjs/common"; import { Collections } from "@src/modules/common/enum/database.collection.enum"; -import { Workspace } from "@src/modules/common/models/workspace.model"; +import { UserDto, Workspace } from "@src/modules/common/models/workspace.model"; import { Db, DeleteResult } from "mongodb"; import { ObjectId, WithId } from "mongodb"; @@ -22,6 +22,26 @@ export class DownGradeWorkspaceRepository { return data; } + async getWorkspacesByIds(ids: string[]): Promise[]> { + if (!ids || ids.length === 0) { + throw new BadRequestException("workspaceIds are required."); + } + const objectIds = ids.map((id) => new ObjectId(id)); + try { + const data = await this.db + .collection(Collections.WORKSPACE) + .find({ _id: { $in: objectIds } }) + .toArray(); + if (!data || data.length === 0) { + throw new BadRequestException("No workspaces found."); + } + return data; + } catch (error) { + console.error("Error fetching workspaces:", error); + throw new BadRequestException("Failed to fetch workspaces."); + } + } + delete(id: string): Promise { const _id = new ObjectId(id); return this.db @@ -46,4 +66,33 @@ export class DownGradeWorkspaceRepository { } return result.value; } + + async updateWorkspaceUsers( + workspaceId: string, + updatedUsers: UserDto[], + ): Promise { + if (!workspaceId) { + throw new BadRequestException("workspaceId is required."); + } + if (!Array.isArray(updatedUsers)) { + throw new BadRequestException("updatedUsers must be an array."); + } + const _id = new ObjectId(workspaceId); + const updatePayload: Partial = { + users: updatedUsers, + updatedAt: new Date(), + }; + try { + const result = await this.db + .collection(Collections.WORKSPACE) + .updateOne({ _id }, { $set: updatePayload }); + if (result.matchedCount === 0) { + throw new BadRequestException("Workspace not found."); + } + return result; + } catch (error) { + console.error("Error updating workspace users:", error); + throw new BadRequestException("Failed to update workspace users."); + } + } } diff --git a/src/modules/billing/services/downgrade.service.ts b/src/modules/billing/services/downgrade.service.ts index ccc7aedf6..ff499ef69 100644 --- a/src/modules/billing/services/downgrade.service.ts +++ b/src/modules/billing/services/downgrade.service.ts @@ -56,6 +56,19 @@ export class DownGradeService { const userFilteredWorkspaces = userData.workspaces.filter( (workspace) => workspace.teamId !== payload.teamId, ); + const workspaces = teamData.workspaces; + for (let workspace of workspaces) { + const workspaceData = await this.downgradeWorkspaceReposiory.get( + workspace.id.toString(), + ); + const updatedUsers = workspaceData.users.filter( + (user) => user.id !== payload.userId, + ); + await this.downgradeWorkspaceReposiory.updateWorkspaceUsers( + workspace.id.toString(), + updatedUsers, + ); + } const userUpdatedParams = { teams: userFilteredTeams, workspaces: userFilteredWorkspaces, diff --git a/src/modules/billing/services/excel-email.service.ts b/src/modules/billing/services/excel-email.service.ts new file mode 100644 index 000000000..4a30d6515 --- /dev/null +++ b/src/modules/billing/services/excel-email.service.ts @@ -0,0 +1,157 @@ +// src/excel/excel.service.ts +import { Injectable } from "@nestjs/common"; +import * as ExcelJS from "exceljs"; +import { + UserExcelDto, + WorkspaceExcelDto, +} from "../payloads/downgrade-user.payload"; + +@Injectable() +export class ExcelEmailService { + /** + * Generate downgrade summary Excel with deleted workspaces and members + */ + async generateDowngradeSummaryExcel( + workspaces: WorkspaceExcelDto[], + users: UserExcelDto[], + ): Promise { + const workbook = new ExcelJS.Workbook(); + // Set workbook properties + workbook.creator = "Sparrow"; + workbook.created = new Date(); + workbook.modified = new Date(); + // ==================== Deleted Workspaces Sheet ==================== + const workspacesSheet = workbook.addWorksheet("Deleted Workspaces", { + properties: { tabColor: { argb: "FF316CF6" } }, + }); + // Define columns for workspaces + workspacesSheet.columns = [ + { header: "Workspace Name", key: "name", width: 35 }, + { header: "Created At", key: "created_at", width: 20 }, + { header: "Collections", key: "collections", width: 15 }, + { header: "Test Flows", key: "testflow", width: 15 }, + ]; + // Style header row for workspaces + const workspaceHeaderRow = workspacesSheet.getRow(1); + workspaceHeaderRow.font = { + bold: true, + color: { argb: "FFFFFFFF" }, + size: 12, + }; + workspaceHeaderRow.fill = { + type: "pattern", + pattern: "solid", + fgColor: { argb: "FF316CF6" }, + }; + workspaceHeaderRow.alignment = { + vertical: "middle", + horizontal: "center", + }; + workspaceHeaderRow.height = 25; + // Add border to header + workspaceHeaderRow.eachCell((cell) => { + cell.border = { + top: { style: "thin" }, + left: { style: "thin" }, + bottom: { style: "thin" }, + right: { style: "thin" }, + }; + }); + // Add workspace data + workspaces.forEach((workspace, index) => { + const row = workspacesSheet.addRow({ + name: workspace.name, + created_at: workspace.created_at || "N/A", + collections: workspace.collections, + testflow: workspace.testflow, + }); + // Alternate row colors for better readability + if (index % 2 === 0) { + row.fill = { + type: "pattern", + pattern: "solid", + fgColor: { argb: "FFF8F9FA" }, + }; + } + // Add borders to data cells + row.eachCell((cell) => { + cell.border = { + top: { style: "thin", color: { argb: "FFE0E0E0" } }, + left: { style: "thin", color: { argb: "FFE0E0E0" } }, + bottom: { style: "thin", color: { argb: "FFE0E0E0" } }, + right: { style: "thin", color: { argb: "FFE0E0E0" } }, + }; + cell.alignment = { vertical: "middle" }; + }); + }); + // ==================== Deleted Members Sheet ==================== + const membersSheet = workbook.addWorksheet("Deleted Members", { + properties: { tabColor: { argb: "FF316CF6" } }, + }); + // Define columns for members + membersSheet.columns = [ + { header: "Name", key: "name", width: 30 }, + { header: "Email", key: "email", width: 40 }, + { header: "Role", key: "role", width: 15 }, + ]; + // Style header row for members + const memberHeaderRow = membersSheet.getRow(1); + memberHeaderRow.font = { + bold: true, + color: { argb: "FFFFFFFF" }, + size: 12, + }; + memberHeaderRow.fill = { + type: "pattern", + pattern: "solid", + fgColor: { argb: "FF316CF6" }, + }; + memberHeaderRow.alignment = { + vertical: "middle", + horizontal: "center", + }; + memberHeaderRow.height = 25; + + // Add border to header + memberHeaderRow.eachCell((cell) => { + cell.border = { + top: { style: "thin" }, + left: { style: "thin" }, + bottom: { style: "thin" }, + right: { style: "thin" }, + }; + }); + + // Add member data + users.forEach((user, index) => { + const row = membersSheet.addRow({ + name: user.name, + email: user.email, + }); + + // Alternate row colors + if (index % 2 === 0) { + row.fill = { + type: "pattern", + pattern: "solid", + fgColor: { argb: "FFF8F9FA" }, + }; + } + + // Add borders to data cells + row.eachCell((cell) => { + cell.border = { + top: { style: "thin", color: { argb: "FFE0E0E0" } }, + left: { style: "thin", color: { argb: "FFE0E0E0" } }, + bottom: { style: "thin", color: { argb: "FFE0E0E0" } }, + right: { style: "thin", color: { argb: "FFE0E0E0" } }, + }; + cell.alignment = { vertical: "middle" }; + }); + }); + + // Generate and return buffer + const buffer = await workbook.xlsx.writeBuffer(); + return Buffer.from(buffer); + } +} diff --git a/src/modules/billing/services/payment-email.service.ts b/src/modules/billing/services/payment-email.service.ts index 9ec2294a0..253ce944c 100644 --- a/src/modules/billing/services/payment-email.service.ts +++ b/src/modules/billing/services/payment-email.service.ts @@ -1,6 +1,8 @@ import { Injectable } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { EmailService } from "@src/modules/common/services/email.service"; +import { ExcelEmailService } from "./excel-email.service"; +import { BlobStorageService } from "@src/modules/common/services/blobStorage.service"; export enum PaymentEmailType { PAYMENT_SUCCESS = "payment_success", @@ -9,6 +11,8 @@ export enum PaymentEmailType { SUBSCRIPTION_RESUBSCRIBED = "subscription_resubscribed", PLAN_UPGRADED = "plan_upgraded", PLAN_DOWNGRADED = "plan_downgraded", + HUB_DOWNGRADED = "hub_downgraded", + HUB_DOWNGRADED_REMOVE_USER = "hub_downgrade_remove_user", UPCOMING_PAYMENT = "upcoming_payment", SUBSCRIPTION_EXPIRED = "subscription_expired", PAYMENT_INFO_UPDATED = "payment_info_updated", @@ -43,6 +47,9 @@ export interface PaymentEmailData { usedSeats?: number; invitedSeats?: number; manageUsersUrl?: string; + workspaces?: any; + users?: any; + sendEmails?: string[]; } @Injectable() @@ -50,6 +57,8 @@ export class PaymentEmailService { constructor( private readonly emailService: EmailService, private readonly configService: ConfigService, + private readonly excelEmailService: ExcelEmailService, + private readonly blobStorageService: BlobStorageService, ) {} /** @@ -81,6 +90,12 @@ export class PaymentEmailService { case PaymentEmailType.PLAN_DOWNGRADED: await this.sendPlanDowngradedEmail(data); break; + case PaymentEmailType.HUB_DOWNGRADED: + await this.sendHubDowngradedEmail(data); + break; + case PaymentEmailType.HUB_DOWNGRADED_REMOVE_USER: + await this.sendHubDowngradeRemovedUserEmail(data); + break; case PaymentEmailType.UPCOMING_PAYMENT: await this.sendUpcomingPaymentActionRequiredEmail(data); break; @@ -330,6 +345,91 @@ export class PaymentEmailService { await this.emailService.sendEmail(transporter, mailOptions); } + /** + * Send hub downgraded email + */ + private async sendHubDowngradedEmail(data: PaymentEmailData): Promise { + // Generate Excel buffer + const excelBuffer = + await this.excelEmailService.generateDowngradeSummaryExcel( + data.workspaces, + data.users, + ); + // Upload to Azure Blob Storage and get URL + const timestamp = new Date() + .toISOString() + .replace(/[:.]/g, "-") + .slice(0, -5); + const storageName = `Downgrade_Summary_${data.hubName}_${timestamp}`; + const downloadName = `Downgrade_Summary_${data.hubName}`; + const mimetype = ".xlsx"; + const blobResult = await this.blobStorageService.uploadExcelBlob( + excelBuffer, + storageName, + downloadName, + mimetype, + ); + console.log("---------------this is the blob result---->", blobResult); + // Send emails to all recipients + const transporter = this.emailService.createTransporter(); + for (const email of data.sendEmails) { + const mailOptions = { + from: this.configService.get("app.senderEmail"), + to: email, + text: "Hub Downgraded", + template: "hubDowngradedEmail", + context: { + firstName: this.extractFirstName(data.ownerName), + hubName: data.hubName, + previousPlanName: data.previousPlanName || "Previous Plan", + newPlanName: data.planName, + effectiveDate: this.formatDate(data.billingPeriodStart), + excelDownloadUrl: blobResult.fileUrl, + sparrowEmail: this.configService.get("support.sparrowEmail"), + sparrowWebsite: this.configService.get("support.sparrowWebsite"), + sparrowWebsiteName: this.configService.get( + "support.sparrowWebsiteName", + ), + }, + subject: `Your Plan for ${data.hubName} has been updated to ${data.planName}`, + }; + await this.emailService.sendEmail(transporter, mailOptions); + } + } + + /** + * Send a Hub downgrade Email after the user is removed from the Hub + */ + private async sendHubDowngradeRemovedUserEmail( + data: PaymentEmailData, + ): Promise { + for (let i = 0; i < data.sendEmails.length; i++) { + const transporter = this.emailService.createTransporter(); + const mailOptions = { + from: this.configService.get("app.senderEmail"), + to: data.sendEmails[i], + text: "Hub Downgraded", + template: "hubDowngradeRemoveUserEmail", + context: { + firstName: this.extractFirstName(data.ownerName), + hubName: data.hubName, + previousPlanName: data.previousPlanName || "Previous Plan", + newPlanName: data.planName, + effectiveDate: data.billingPeriodStart + ? this.formatDate(data.billingPeriodStart) + : this.formatDate(data.billingPeriodStart), + sparrowEmail: this.configService.get("support.sparrowEmail"), + sparrowWebsite: this.configService.get("support.sparrowWebsite"), + sparrowWebsiteName: this.configService.get( + "support.sparrowWebsiteName", + ), + }, + subject: `Your Plan for ${data.hubName} has been updated to ${data.planName}`, + }; + await this.emailService.sendEmail(transporter, mailOptions); + } + } + /** * Send upcoming payment action required email */ diff --git a/src/modules/billing/services/stripe-subscription.service.ts b/src/modules/billing/services/stripe-subscription.service.ts index 630589d25..36d24bea8 100644 --- a/src/modules/billing/services/stripe-subscription.service.ts +++ b/src/modules/billing/services/stripe-subscription.service.ts @@ -21,6 +21,12 @@ import { UserDto } from "@src/modules/common/models/user.model"; import { DownGradeService } from "./downgrade.service"; import { DownGradeTeamRepository } from "../repositories/downgradeTeam.repository"; import { ObjectId } from "mongodb"; +import { + UserExcelDto, + WorkspaceExcelDto, +} from "../payloads/downgrade-user.payload"; +import { DownGradeWorkspaceRepository } from "../repositories/downgradeWorkspace.repository"; +import { DownGradeUserRepository } from "../repositories/downgradeUser.repository"; // Dynamically import Stripe service class let StripeService: any; @@ -41,6 +47,8 @@ export class StripeSubscriptionService { private readonly paymentEmailHelper: PaymentEmailHelper, private readonly downgradeService: DownGradeService, private readonly downgradeTeamRepository: DownGradeTeamRepository, + private readonly downGradeWorkspaceRepository: DownGradeWorkspaceRepository, + private readonly downGradeUserRepository: DownGradeUserRepository, @Optional() @Inject(StripeService) private readonly stripeService?: any, ) {} @@ -203,7 +211,6 @@ export class StripeSubscriptionService { metadata: any, eventId?: string, ): Promise { - console.log("this is the subscription of cancekk------->"); // Get current team state const currentTeam = await this.stripeSubscriptionRepo.findTeamById( metadata.hubId, @@ -585,7 +592,13 @@ export class StripeSubscriptionService { team?.downgrade?.downgradeType === SubscriptionDowngradeType.MANUAL; if (hasDowngradeConfig && isManualDowngrade) { isDowngrading = true; - await this.executeManualDowngrade(team, metadata.hubId); + await this.executeManualDowngrade( + team, + metadata.hubId, + previousPlan, + newPlan, + new Date(), + ); await this.stripeSubscriptionRepo.removeDowngradeDetails( metadata.hubId, ); @@ -971,7 +984,13 @@ export class StripeSubscriptionService { metadata.hubId, ); // Execute manual downgrade AFTER plan change is complete - await this.executeManualDowngrade(team, metadata.hubId); + await this.executeManualDowngrade( + team, + metadata.hubId, + communityPlan.name, + team.plan.name, + new Date(), + ); const previousPlan = team?.plan?.name || "unknown"; // Get cancellation reason if available @@ -2073,6 +2092,9 @@ export class StripeSubscriptionService { private async executeManualDowngrade( team: Team, hubId: string, + previousPlan?: string, + currentPlan?: string, + startDate?: Date, ): Promise { const downgrade = team?.downgrade; if ( @@ -2111,12 +2133,20 @@ export class StripeSubscriptionService { team?.users ?.filter((user: UserDto) => user.role !== "owner") .map((user: UserDto) => user.id) || []; - + const OwnerEmail = team.users[0].email; // Extract workspace IDs from downgrade list (workspaces to keep) const downgradeWorkspaceIds = teamDowngradeWorkspaces?.map((ws) => ws.id) || []; // Extract user IDs from downgrade list (users to keep) const downgradeUserIds = teamDowngradeUsers?.map((user) => user.id) || []; + const downgradeUserEmails = teamDowngradeUsers.map((user) => user.email); + const nonDowngradedUsersWithEmail = + team?.users + ?.filter( + (user: UserDto) => + user.role !== "owner" && !downgradeUserIds.includes(user.id), + ) + .map((user: UserDto) => user.email) || []; // Workspaces not in the downgrade list (these will be deleted) const nonDowngradedWorkspaces = allWorkspaces.filter( (wsId: string) => !downgradeWorkspaceIds.includes(wsId), @@ -2151,6 +2181,20 @@ export class StripeSubscriptionService { } } } + const workspaceExcelData = await this.workspaceExcelData( + nonDowngradedWorkspaces, + ); + const userExcelData = await this.userExcelData(nonDowngradedUsers); + await this.sendEmailsToUserHubDowngrade( + previousPlan, + currentPlan, + team, + startDate, + workspaceExcelData, + userExcelData, + [...downgradeUserEmails, OwnerEmail], + nonDowngradedUsersWithEmail, + ); console.log( `Manual downgrade completed for team ${hubId}: ${nonDowngradedWorkspaces.length} workspaces deleted, ${nonDowngradedUsers.length} users removed`, ); @@ -2184,4 +2228,88 @@ export class StripeSubscriptionService { return newLevel < previousLevel; } + + private async workspaceExcelData( + workspaceIds: string[], + ): Promise { + if (workspaceIds.length === 0) { + return [ + { + name: "", + created_at: "", + collections: 0, + testflow: 0, + }, + ]; + } + const workspaces = + await this.downGradeWorkspaceRepository.getWorkspacesByIds(workspaceIds); + const resultWorkspaces: WorkspaceExcelDto[] = workspaces.map( + (workspace) => ({ + name: workspace.name, + created_at: workspace.createdAt + ? workspace.createdAt.toUTCString() + : "N/A", + collections: workspace?.collection?.length || 0, + testflow: workspace?.testflows?.length || 0, + }), + ); + return resultWorkspaces; + } + + private async userExcelData(userIds: string[]): Promise { + if (userIds.length === 0) { + return [ + { + name: "", + email: "", + }, + ]; + } + const usersData = + await this.downGradeUserRepository.findUsersByStringIds(userIds); + const resultUsers: UserExcelDto[] = usersData.map((user) => ({ + name: user.name || "N/A", + email: user.email || "N/A", + })); + return resultUsers; + } + + /** + * Helper method to determine if a send Email after downgrade to Hub + * @param previousPlan The previous plan object. + * @param newPlan The new plan object. + * @param currentUsers downgraded users. + * @param removedUser removed users from Hub. + * @returns Boolean indicating if this is a downgrade + */ + private async sendEmailsToUserHubDowngrade( + previousPlan: string, + newPlan: string, + team: Team, + startDate: Date, + workspaces?: WorkspaceExcelDto[], + users?: UserExcelDto[], + currentUser?: string[], + removedUser?: string[], + ) { + await this.paymentEmailHelper.sendHubDowngradedEmail( + team, + startDate, + previousPlan, + newPlan, + currentUser, + workspaces, + users, + ); + if (removedUser.length > 0) { + await this.paymentEmailHelper.sendHubDowngradeRemoveUserEmail( + team, + startDate, + previousPlan, + newPlan, + removedUser, + ); + } + } } diff --git a/src/modules/common/services/blobStorage.service.ts b/src/modules/common/services/blobStorage.service.ts index 598f28897..495fab470 100644 --- a/src/modules/common/services/blobStorage.service.ts +++ b/src/modules/common/services/blobStorage.service.ts @@ -17,6 +17,7 @@ export class BlobStorageService { private blobServiceClient: BlobServiceClient; private containerClient: ContainerClient; private aiContainerClient: ContainerClient; + private downGradeHubClient: ContainerClient; /** * Constructor to initialize BlobStorageService with required dependencies. @@ -30,8 +31,11 @@ export class BlobStorageService { "feedbackBlob.container", ); const aiConversationBLobContainer = this.configService.get( - "ai.conversationConatiner" - ) + "ai.conversationConatiner", + ); + // const downgradeHubBlobContainer = this.configService.get( + // "downgradeHub.container", + // ); try { /** @@ -63,10 +67,21 @@ export class BlobStorageService { ); if (!aiConversationBLobContainer) { - console.warn("AI Conversation Blob is disabled: No container provided."); + console.warn( + "AI Conversation Blob is disabled: No container provided.", + ); return; } + // const downgradeHubBlobContainer = this.configService.get( + // "downgradeHub.container", + // ); + + // if (!downgradeHubBlobContainer) { + // console.warn("Downgrade Blob is disabled: No container provided."); + // return; + // } + this.blobServiceClient = BlobServiceClient.fromConnectionString( azureConnectionString, ); @@ -82,6 +97,9 @@ export class BlobStorageService { this.aiContainerClient = this.blobServiceClient.getContainerClient( aiConversationBLobContainer, ); + // this.downGradeHubClient = this.blobServiceClient.getContainerClient( + // downgradeHubBlobContainer, + // ); } catch (e) { console.error(e); } @@ -177,6 +195,56 @@ export class BlobStorageService { return docURL; } + /** + * Uploads an Excel Document to Azure Blob Storage. + * @param buffer - Buffer containing the Excel file data + * @param storageName - Name used for storing the file in blob (with timestamp) + * @param downloadName - Name shown when user downloads the file (clean name) + * @param mimetype - MIME type of the file (Excel format) or file extension (e.g., ".xlsx") + * @returns Object containing fileId and fileUrl + */ + async uploadExcelBlob( + buffer: Buffer, + storageName: string, + downloadName: string, + mimetype: string, + ): Promise<{ fileUrl: string; fileId: string }> { + const fileId = uuidv4(); + // Handle both full MIME type and file extension + let fileExtension: string; + let contentType: string; + if (mimetype.startsWith(".")) { + // If mimetype is an extension like ".xlsx" + fileExtension = mimetype.substring(1); // Remove the dot + contentType = + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + } else { + // If mimetype is a full MIME type + fileExtension = await this.getFileExtension(mimetype); + contentType = mimetype; + } + const uniqueFileName = `${fileId}-${storageName}.${fileExtension}`; + if (!this.aiContainerClient) { + throw new BadRequestException( + "Azure blob container is not connected to backend server.", + ); + } + const blockBlobClient = + this.aiContainerClient.getBlockBlobClient(uniqueFileName); + // Set Content-Type and Content-Disposition headers for Excel download + const uploadOptions = { + blobHTTPHeaders: { + blobContentType: contentType, + blobContentDisposition: `attachment; filename="${downloadName}.${fileExtension}"`, + }, + }; + await blockBlobClient.upload(buffer, buffer.length, uploadOptions); + return { + fileId: fileId, + fileUrl: blockBlobClient.url, + }; + } + async deleteAiDocByUrl(fileUrl: string): Promise { if (!this.aiContainerClient) { throw new BadRequestException( diff --git a/src/modules/views/hubDowngradeRemoveUserEmail.handlebars b/src/modules/views/hubDowngradeRemoveUserEmail.handlebars new file mode 100644 index 000000000..1bfdc87c9 --- /dev/null +++ b/src/modules/views/hubDowngradeRemoveUserEmail.handlebars @@ -0,0 +1,143 @@ +{{!< layoutName}} + + + + + + Plan Downgraded User + + + + + +
+
+
+ + {{> headerV2}} + + + + +
+ + + + +
+
+

Your {{hubName}} Hub Has Been Downgraded


+
+
+
+
+ + + + + +
+ +
+ + + + +
+ +
+
+ {{> footerV2}} +
+
+
+ + \ No newline at end of file diff --git a/src/modules/views/hubDowngradedEmail.handlebars b/src/modules/views/hubDowngradedEmail.handlebars new file mode 100644 index 000000000..90588a447 --- /dev/null +++ b/src/modules/views/hubDowngradedEmail.handlebars @@ -0,0 +1,201 @@ +{{!< layoutName}} + + + + + + Plan Downgraded + + + + + +
+
+
+ + {{> headerV2}} + + + + +
+ + + + +
+
+

Your {{hubName}} Hub Has Been Downgraded


+
+
+
+
+ + + + + +
+ +
+ + + + + + + + +
+
+
+

+ For a detailed list of all the content and users affected by this change, you can view a full summary report. +

+
+ + + + +
+
+ +
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ {{> footerV2}} +
+
+
+ + \ No newline at end of file From 6ee9f9114fbcbb29c9ee89a53a4c32b243742390 Mon Sep 17 00:00:00 2001 From: "aakash.reddy@techdome.net.in" Date: Thu, 16 Oct 2025 16:25:38 +0530 Subject: [PATCH 18/20] feat: remove user function updation --- .env.example | 1 + .../payloads/downgrade-user.payload.ts | 2 +- .../repositories/downgradeUser.repository.ts | 39 ++++++++++- .../downgradeWorkspace.repository.ts | 14 +++- .../billing/services/downgrade.service.ts | 68 +++++++++---------- .../services/stripe-subscription.service.ts | 22 +++--- src/modules/common/config/configuration.ts | 7 +- .../common/services/blobStorage.service.ts | 27 ++++---- 8 files changed, 107 insertions(+), 73 deletions(-) diff --git a/.env.example b/.env.example index f7cf5750a..ad8309448 100755 --- a/.env.example +++ b/.env.example @@ -80,6 +80,7 @@ AZURE_CONNECTION_STRING= # Azure Storage connection string AZURE_INSIGHTS_CONNECTION_STRING= # Azure Application Insights connection string FEEDBACK_BLOB_CONTAINER= # Azure Blob container name for feedback storage AI_CONVERSATION_BLOB_CONTAINER= # Azure Blob container name for AI Conversation storage +DOWNGRADE_HUB_BLOB_CONTAINER= # Azure Blob container name for downgrade Hub Details storage. # [AI SERVICES] # Azure OpenAI Configuration diff --git a/src/modules/billing/payloads/downgrade-user.payload.ts b/src/modules/billing/payloads/downgrade-user.payload.ts index 36347e07e..ee070fee6 100644 --- a/src/modules/billing/payloads/downgrade-user.payload.ts +++ b/src/modules/billing/payloads/downgrade-user.payload.ts @@ -21,7 +21,7 @@ export class DowngradeUserDto { @ApiProperty({ example: "64f03af32e420f7f68055b92" }) @IsMongoId() @IsNotEmpty() - userId: string; + userIds: string[]; } export class TourGuideDto { diff --git a/src/modules/billing/repositories/downgradeUser.repository.ts b/src/modules/billing/repositories/downgradeUser.repository.ts index ca19dea3b..09c1cdb73 100644 --- a/src/modules/billing/repositories/downgradeUser.repository.ts +++ b/src/modules/billing/repositories/downgradeUser.repository.ts @@ -1,9 +1,10 @@ import { Injectable, Inject, BadRequestException } from "@nestjs/common"; import { Db } from "mongodb"; import { ObjectId, WithId } from "mongodb"; -import { User } from "@src/modules/common/models/user.model"; +import { User, UserWorkspaceDto } from "@src/modules/common/models/user.model"; import { Collections } from "@src/modules/common/enum/database.collection.enum"; import { DownGradeUserTourGuideDto } from "../payloads/downgrade-user.payload"; +import { TeamDto } from "@src/modules/common/models/team.model"; /** * Repository for handling promo code database operations @@ -31,6 +32,42 @@ export class DownGradeUserRepository { return responseData.value; } + async updateUserTeamsAndWorkspaces( + userId: ObjectId, + updatedTeams: TeamDto[], + updatedWorkspaces: UserWorkspaceDto[], + ): Promise> { + if (!userId) { + throw new BadRequestException("User ID is required."); + } + if (!Array.isArray(updatedTeams)) { + throw new BadRequestException("updatedTeams must be an array."); + } + if (!Array.isArray(updatedWorkspaces)) { + throw new BadRequestException("updatedWorkspaces must be an array."); + } + const updatePayload: Partial = { + teams: updatedTeams, + workspaces: updatedWorkspaces, + updatedAt: new Date(), + }; + try { + const result = await this.db + .collection(Collections.USER) + .findOneAndUpdate( + { _id: userId }, + { $set: updatePayload }, + { returnDocument: "after" }, + ); + if (!result.value) { + throw new BadRequestException("User not found."); + } + return result.value; + } catch (error) { + console.error("Error updating user teams and workspaces:", error); + } + } + async findUsersByIdArray(IdArray: Array): Promise[]> { const response = await this.db .collection(Collections.USER) diff --git a/src/modules/billing/repositories/downgradeWorkspace.repository.ts b/src/modules/billing/repositories/downgradeWorkspace.repository.ts index 04235ed7e..5a0c40492 100644 --- a/src/modules/billing/repositories/downgradeWorkspace.repository.ts +++ b/src/modules/billing/repositories/downgradeWorkspace.repository.ts @@ -1,6 +1,6 @@ import { Injectable, Inject, BadRequestException } from "@nestjs/common"; import { Collections } from "@src/modules/common/enum/database.collection.enum"; -import { UserDto, Workspace } from "@src/modules/common/models/workspace.model"; +import { AdminDto, UserDto, Workspace } from "@src/modules/common/models/workspace.model"; import { Db, DeleteResult } from "mongodb"; import { ObjectId, WithId } from "mongodb"; @@ -70,6 +70,7 @@ export class DownGradeWorkspaceRepository { async updateWorkspaceUsers( workspaceId: string, updatedUsers: UserDto[], + updatedAdmins?: AdminDto[], ): Promise { if (!workspaceId) { throw new BadRequestException("workspaceId is required."); @@ -77,11 +78,19 @@ export class DownGradeWorkspaceRepository { if (!Array.isArray(updatedUsers)) { throw new BadRequestException("updatedUsers must be an array."); } + if (updatedAdmins && !Array.isArray(updatedAdmins)) { + throw new BadRequestException( + "updatedAdmins must be an array when provided.", + ); + } const _id = new ObjectId(workspaceId); const updatePayload: Partial = { users: updatedUsers, updatedAt: new Date(), }; + if (updatedAdmins) { + updatePayload.admins = updatedAdmins; + } try { const result = await this.db .collection(Collections.WORKSPACE) @@ -91,8 +100,7 @@ export class DownGradeWorkspaceRepository { } return result; } catch (error) { - console.error("Error updating workspace users:", error); - throw new BadRequestException("Failed to update workspace users."); + console.error("Error updating workspace users/admins:", error); } } } diff --git a/src/modules/billing/services/downgrade.service.ts b/src/modules/billing/services/downgrade.service.ts index ff499ef69..cbf74c4f8 100644 --- a/src/modules/billing/services/downgrade.service.ts +++ b/src/modules/billing/services/downgrade.service.ts @@ -25,36 +25,13 @@ export class DownGradeService { const teamFilter = new ObjectId(payload.teamId); const teamData = await this.downgradeTeamRepository.findTeamByTeamId(teamFilter); - const userFilter = new ObjectId(payload.userId); - const userData = - await this.downgradeUserRepository.findUserByUserId(userFilter); - const teamAdmins = [...teamData.admins]; - let userTeamRole; - for (const item of userData.teams) { - if (item.id.toString() === payload.teamId) { - userTeamRole = item.role; - } - } - const teamUser = [...teamData.users]; - let filteredAdmin; - const filteredData = teamUser.filter( - (item) => item.id.toString() !== payload.userId.toString(), - ); - if (userTeamRole === TeamRole.ADMIN) { - filteredAdmin = teamAdmins.filter( - (id: string) => id.toString() !== payload.userId.toString(), - ); - } - const teamUpdatedParams = { - users: filteredData, - admins: userTeamRole === TeamRole.ADMIN ? filteredAdmin : teamAdmins, - }; - const userTeams = [...userData.teams]; - const userFilteredTeams = userTeams.filter( - (item) => item.id.toString() !== payload.teamId.toString(), + const teamUsersData = teamData.users; + const teamAdminsData = teamData.admins; + const updateTeamUsersData = teamUsersData.filter( + (user) => !payload.userIds.includes(user.id), ); - const userFilteredWorkspaces = userData.workspaces.filter( - (workspace) => workspace.teamId !== payload.teamId, + const updateAdminUserData = teamAdminsData.filter( + (admin) => !payload.userIds.includes(admin), ); const workspaces = teamData.workspaces; for (let workspace of workspaces) { @@ -62,21 +39,38 @@ export class DownGradeService { workspace.id.toString(), ); const updatedUsers = workspaceData.users.filter( - (user) => user.id !== payload.userId, + (user) => !payload.userIds.includes(user.id), + ); + const updatedAdmins = workspaceData.admins.filter( + (admin) => !payload.userIds.includes(admin.id), ); await this.downgradeWorkspaceReposiory.updateWorkspaceUsers( workspace.id.toString(), updatedUsers, + updatedAdmins, ); } - const userUpdatedParams = { - teams: userFilteredTeams, - workspaces: userFilteredWorkspaces, + for (let user of payload.userIds) { + const userObject = new ObjectId(user); + const userData = + await this.downgradeUserRepository.findUserByUserId(userObject); + const updateUserTeams = userData.teams.filter( + (usr) => usr.id.toString() != payload.teamId, + ); + const updateUserWorkspaces = userData.workspaces.filter( + (workspace) => + !workspaces.some((w) => w.id.toString() === workspace.workspaceId), + ); + await this.downgradeUserRepository.updateUserTeamsAndWorkspaces( + userObject, + updateUserTeams, + updateUserWorkspaces, + ); + } + const teamUpdatedParams = { + users: updateTeamUsersData, + admins: updateAdminUserData, }; - await this.downgradeUserRepository.updateUserById( - userFilter, - userUpdatedParams, - ); const data = await this.downgradeTeamRepository.updateTeamById( teamFilter, teamUpdatedParams, diff --git a/src/modules/billing/services/stripe-subscription.service.ts b/src/modules/billing/services/stripe-subscription.service.ts index 36d24bea8..43029dc21 100644 --- a/src/modules/billing/services/stripe-subscription.service.ts +++ b/src/modules/billing/services/stripe-subscription.service.ts @@ -2165,20 +2165,14 @@ export class StripeSubscriptionService { // Remove users not in the downgrade list if (nonDowngradedUsers.length > 0 && teamDowngradeUsers.length > 0) { - for (const userId of nonDowngradedUsers) { - try { - const payload = { - teamId: hubId, - userId: userId, - }; - await this.downgradeService.removeUserFromTeam(payload); - } catch (error) { - console.error( - `Error removing user ${userId} from team ${hubId}:`, - error, - ); - // Continue with other users even if one fails - } + try { + const payload = { + teamId: hubId, + userIds: nonDowngradedUsers, + }; + await this.downgradeService.removeUserFromTeam(payload); + } catch (error) { + console.error(`Error removing users from team ${hubId}:`, error); } } const workspaceExcelData = await this.workspaceExcelData( diff --git a/src/modules/common/config/configuration.ts b/src/modules/common/config/configuration.ts index 814a2c486..e150f252d 100644 --- a/src/modules/common/config/configuration.ts +++ b/src/modules/common/config/configuration.ts @@ -141,10 +141,13 @@ export default () => ({ adminEmail: process.env.SELF_HOST_ADMIN_EMAIL, adminPassword: process.env.SELF_HOST_ADMIN_PASSWORD, }, - sparrowProxy:{ - baseUrl: process.env.SPARROW_PROXY_BASE_URL + sparrowProxy: { + baseUrl: process.env.SPARROW_PROXY_BASE_URL, }, trial: { trialPeriod: 14, }, + downgradeHub: { + container: process.env.DOWNGRADE_HUB_BLOB_CONTAINER, + }, }); diff --git a/src/modules/common/services/blobStorage.service.ts b/src/modules/common/services/blobStorage.service.ts index 495fab470..140c41844 100644 --- a/src/modules/common/services/blobStorage.service.ts +++ b/src/modules/common/services/blobStorage.service.ts @@ -33,9 +33,6 @@ export class BlobStorageService { const aiConversationBLobContainer = this.configService.get( "ai.conversationConatiner", ); - // const downgradeHubBlobContainer = this.configService.get( - // "downgradeHub.container", - // ); try { /** @@ -73,14 +70,14 @@ export class BlobStorageService { return; } - // const downgradeHubBlobContainer = this.configService.get( - // "downgradeHub.container", - // ); + const downgradeHubBlobContainer = this.configService.get( + "downgradeHub.container", + ); - // if (!downgradeHubBlobContainer) { - // console.warn("Downgrade Blob is disabled: No container provided."); - // return; - // } + if (!downgradeHubBlobContainer) { + console.warn("Downgrade Blob is disabled: No container provided."); + return; + } this.blobServiceClient = BlobServiceClient.fromConnectionString( azureConnectionString, @@ -97,9 +94,9 @@ export class BlobStorageService { this.aiContainerClient = this.blobServiceClient.getContainerClient( aiConversationBLobContainer, ); - // this.downGradeHubClient = this.blobServiceClient.getContainerClient( - // downgradeHubBlobContainer, - // ); + this.downGradeHubClient = this.blobServiceClient.getContainerClient( + downgradeHubBlobContainer, + ); } catch (e) { console.error(e); } @@ -224,13 +221,13 @@ export class BlobStorageService { contentType = mimetype; } const uniqueFileName = `${fileId}-${storageName}.${fileExtension}`; - if (!this.aiContainerClient) { + if (!this.downGradeHubClient) { throw new BadRequestException( "Azure blob container is not connected to backend server.", ); } const blockBlobClient = - this.aiContainerClient.getBlockBlobClient(uniqueFileName); + this.downGradeHubClient.getBlockBlobClient(uniqueFileName); // Set Content-Type and Content-Disposition headers for Excel download const uploadOptions = { blobHTTPHeaders: { From d4b57f8f811d30e0d8e5c1188ae76f42f7ccc9f3 Mon Sep 17 00:00:00 2001 From: Kishan Gupta Date: Thu, 16 Oct 2025 16:46:53 +0530 Subject: [PATCH 19/20] add user guide in prescript and test assertions --- .../payloads/downgrade-user.payload.ts | 4 +++ src/modules/common/models/collection.model.ts | 4 +++ src/modules/common/models/user.model.ts | 5 +++- .../identity/controllers/user.controller.ts | 27 +++++++++++++++++++ src/modules/identity/services/user.service.ts | 15 +++++++++++ .../controllers/ai-assistant.controller.ts | 2 +- .../workspace/services/collection.service.ts | 10 ++++++- 7 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/modules/billing/payloads/downgrade-user.payload.ts b/src/modules/billing/payloads/downgrade-user.payload.ts index b59cf5c4c..ca40e8683 100644 --- a/src/modules/billing/payloads/downgrade-user.payload.ts +++ b/src/modules/billing/payloads/downgrade-user.payload.ts @@ -36,6 +36,10 @@ export class TourGuideDto { @IsBoolean() @IsOptional() isRequestTestsScriptDemoCompleted?: boolean; + + @IsBoolean() + @IsOptional() + isRequestAssertionsDemoCompleted?: boolean; } class UserWorkspaceDto { diff --git a/src/modules/common/models/collection.model.ts b/src/modules/common/models/collection.model.ts index 65ad50d77..c34435d13 100644 --- a/src/modules/common/models/collection.model.ts +++ b/src/modules/common/models/collection.model.ts @@ -1246,6 +1246,10 @@ export class CollectionWithRequestTestsOption extends CollectionGenerateVariable @IsBoolean() @IsOptional() isRequestTestsScriptDemoCompleted?: boolean; + + @IsBoolean() + @IsOptional() + isRequestAssertionsDemoCompleted?: boolean; } export class CollectionDto { diff --git a/src/modules/common/models/user.model.ts b/src/modules/common/models/user.model.ts index eb3706429..68854fd7b 100644 --- a/src/modules/common/models/user.model.ts +++ b/src/modules/common/models/user.model.ts @@ -45,8 +45,11 @@ export class TourGuideDto { @IsBoolean() @IsOptional() isRequestTestsScriptDemoCompleted?: boolean; -} + @IsBoolean() + @IsOptional() + isRequestAssertionsDemoCompleted?: boolean; +} export class User { @IsString() diff --git a/src/modules/identity/controllers/user.controller.ts b/src/modules/identity/controllers/user.controller.ts index cf3553b9c..d5e10fbb1 100644 --- a/src/modules/identity/controllers/user.controller.ts +++ b/src/modules/identity/controllers/user.controller.ts @@ -587,4 +587,31 @@ export class UserController { ); return res.status(HttpStatusCode.OK).send(responseData); } + + @Post("/request-assertions-demo") + @ApiOperation({ + summary: "When the User has completed the Assertions Demo.", + description: + "Marks the property as true when the user has completed the assertions demo.", + }) + @UseGuards(JwtAuthGuard) + @ApiResponse({ + status: 200, + description: "Assertions Demo completed successfully", + }) + @ApiResponse({ status: 400, description: "Bad Request" }) + async requestAssertionsDemoCompleted( + @Req() request: ExtendedFastifyRequest, + @Res() res: FastifyReply, + ) { + const user = request.user; + const result = await this.userService.requestAssertionsDemoCompleted( + user.email, + ); + const responseData = new ApiResponseService( + "Assertions Demo completed successfully", + HttpStatusCode.OK, + result, + ); + } } diff --git a/src/modules/identity/services/user.service.ts b/src/modules/identity/services/user.service.ts index 613b20d70..08c996653 100644 --- a/src/modules/identity/services/user.service.ts +++ b/src/modules/identity/services/user.service.ts @@ -825,4 +825,19 @@ export class UserService { }); return response; } + + async requestAssertionsDemoCompleted(email: string) { + const userDetails = await this.userRepository.getUserByEmail(email); + if (!userDetails) { + throw new BadRequestException("User does not exist"); + } + const updatedTourGuide = { + ...userDetails.tourGuide, + isRequestAssertionsDemoCompleted: true, + }; + const response = await this.userRepository.updateUserById(userDetails._id, { + tourGuide: updatedTourGuide, + }); + return response; + } } diff --git a/src/modules/workspace/controllers/ai-assistant.controller.ts b/src/modules/workspace/controllers/ai-assistant.controller.ts index 5226fc0af..e42949cb8 100644 --- a/src/modules/workspace/controllers/ai-assistant.controller.ts +++ b/src/modules/workspace/controllers/ai-assistant.controller.ts @@ -287,7 +287,7 @@ export class AiAssistantController { description: "Server failed to generate pre-script.", }) async generatePreScriptForRequest( - @Body() content: generateTestCasesDto + @Body() content: generateTestCasesDto, @Res() res: FastifyReply, @Req() request: ExtendedFastifyRequest, ) { diff --git a/src/modules/workspace/services/collection.service.ts b/src/modules/workspace/services/collection.service.ts index 588d5e09d..ce7f64dea 100644 --- a/src/modules/workspace/services/collection.service.ts +++ b/src/modules/workspace/services/collection.service.ts @@ -521,6 +521,10 @@ export class CollectionService { ?.isRequestTestsScriptDemoCompleted ? false : true; + collection.isRequestAssertionsDemoCompleted = userDetails?.tourGuide + ?.isRequestAssertionsDemoCompleted + ? false + : true; let alreadyProcessed = false; if ( userDetails?.isGenerateVariableTrial && @@ -596,6 +600,10 @@ export class CollectionService { ?.isRequestTestsNoCodeDemoCompleted ? false : true; + collections[i].isRequestAssertionsDemoCompleted = userDetails?.tourGuide + ?.isRequestAssertionsDemoCompleted + ? false + : true; let alreadyProcessed = false; // Case 1: Trial array exists and contains collectionId if ( @@ -1387,7 +1395,7 @@ export class CollectionService { const validGeneratedPairs = generatedPairs.filter( (pair) => pair.key?.trim() && pair.value?.trim(), ); - if(validGeneratedPairs.length < 1){ + if (validGeneratedPairs.length < 1) { throw new BadRequestException( "Please provide Vaild Generated Variables.", ); From 0ac3c801b0aa7641c4d2fce60c06fc8012c554e5 Mon Sep 17 00:00:00 2001 From: Kishan Gupta Date: Thu, 16 Oct 2025 17:31:27 +0530 Subject: [PATCH 20/20] add the instruction for generating prescript --- .../workspace/services/ai-assistant.service.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/modules/workspace/services/ai-assistant.service.ts b/src/modules/workspace/services/ai-assistant.service.ts index 198b0fe85..0250de954 100644 --- a/src/modules/workspace/services/ai-assistant.service.ts +++ b/src/modules/workspace/services/ai-assistant.service.ts @@ -2561,17 +2561,6 @@ export class AiAssistantService { if (!content.text?.trim()) { throw new BadRequestException("prompt must be provided."); } - - // You can move this to a separate instructions file if needed - const generatePreScriptInstructions = ` - You are an assistant specialized in generating pre-scripts for API requests. - Given the API details and requirements, generate ONLY the pre-script code needed to set up the request environment. - STRICT RULES: - - Do NOT include explanations, markdown, comments, or any extra text. - - Output ONLY the raw script/code required for the pre-request setup. - - No text outside of the required output format is allowed. - `; - try { const response = await this.deepseekClient .path("/chat/completions")