Skip to content

Commit 7f6cfe9

Browse files
Merge pull request #856 from aakashreddy-p/aakash/feat/maunal-downgrade-setup
feat: manual-downgrade,auto-downgrade plan setup SPRW-2164.
2 parents a7ba2f2 + 7b30995 commit 7f6cfe9

22 files changed

+947
-45
lines changed

src/modules/billing/billing.module.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ import { UserRepository } from "@src/modules/identity/repositories/user.reposito
2222
import { PricingService } from "@src/modules/workspace/services/pricing.repository";
2323
import { PricingRepository } from "@src/modules/workspace/repositories/pricing.repository";
2424
import { SalesEmailRepository } from "../workspace/repositories/sales-email.repository";
25+
import { DownGradeTeamRepository } from "./repositories/downgradeTeam.repository";
26+
import { DownGradeUserRepository } from "./repositories/downgradeUser.repository";
27+
import { DownGradeWorkspaceRepository } from "./repositories/downgradeWorkspace.repository";
28+
import { DownGradeService } from "./services/downgrade.service";
2529

2630
// Try to import the Stripe module, but don't crash if it's not available
2731
let StripeModule: any;
@@ -48,6 +52,10 @@ export class BillingModule {
4852
BillingAuditRepository,
4953
PromoCodeRepository,
5054
PricingRepository,
55+
DownGradeTeamRepository,
56+
DownGradeUserRepository,
57+
DownGradeWorkspaceRepository,
58+
DownGradeService,
5159
BillingAuditService,
5260
PromoCodeService,
5361
PricingService,
@@ -73,6 +81,10 @@ export class BillingModule {
7381
BillingAuditRepository,
7482
BillingAuditService,
7583
PromoCodeService,
84+
DownGradeTeamRepository,
85+
DownGradeUserRepository,
86+
DownGradeWorkspaceRepository,
87+
DownGradeService,
7688
PricingService,
7789
PaymentEmailService,
7890
PaymentEmailHelper,

src/modules/billing/controllers/stripe.controller.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import { SalesEmailRepository } from "@src/modules/workspace/repositories/sales-
4949
import { ConfigService } from "@nestjs/config";
5050
import { TrialType } from "@src/modules/common/enum/trial.enum";
5151
import { PricingPlan } from "@src/modules/common/models/pricing.model";
52+
import { DownGradeService } from "../services/downgrade.service";
5253

5354
// Dynamically import Stripe services
5455
let StripeService: any;
@@ -72,6 +73,7 @@ export class StripeController {
7273
private readonly pricingService: PricingService,
7374
private readonly configService: ConfigService,
7475
private readonly salesEmailRepository: SalesEmailRepository,
76+
private readonly downgradeService: DownGradeService,
7577
) {
7678
this.isStripeAvailable = !!this.stripeService;
7779

@@ -295,10 +297,10 @@ export class StripeController {
295297
for (const planBilling of currentPlan.billing) {
296298
if (planBilling.providers?.stripe === createSubscriptionDto.priceId) {
297299
selectedPlan = currentPlan;
298-
break;
300+
break;
299301
}
300302
}
301-
if (selectedPlan) break;
303+
if (selectedPlan) break;
302304
}
303305
if (selectedPlan) {
304306
// Replace or set planName
@@ -423,6 +425,13 @@ export class StripeController {
423425
updateSubscriptionDto.seats,
424426
updateSubscriptionDto.paymentBehavior,
425427
);
428+
if (subscription && updateSubscriptionDto?.workspaces) {
429+
await this.downgradeService.addDowgradeDetails(
430+
updateSubscriptionDto?.metadata?.hubId,
431+
updateSubscriptionDto?.workspaces,
432+
updateSubscriptionDto?.users,
433+
);
434+
}
426435

427436
return subscription;
428437
} catch (error) {
@@ -459,12 +468,17 @@ export class StripeController {
459468
): Promise<SubscriptionResponseDto> {
460469
try {
461470
this.checkStripeAvailability();
462-
463471
const subscription = await this.stripeService.cancelSubscription(
464472
subscriptionId,
465473
false, //disables cancellation at mid cycle
466474
);
467-
475+
if (subscription) {
476+
await this.downgradeService.addDowgradeDetails(
477+
cancelSubscriptionDto.teamId,
478+
cancelSubscriptionDto.workspaces,
479+
cancelSubscriptionDto.users,
480+
);
481+
}
468482
return { subscription };
469483
} catch (error) {
470484
throw new HttpException(
@@ -509,7 +523,11 @@ export class StripeController {
509523
subscriptionId,
510524
reactivateDto.metadata,
511525
);
512-
526+
if (subscription) {
527+
await this.downgradeService.removeDowngradeDetails(
528+
reactivateDto.metadata?.hubId,
529+
);
530+
}
513531
return { subscription };
514532
} catch (error) {
515533
throw new HttpException(
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { IsNotEmpty, IsMongoId } from "class-validator";
2+
import { ApiProperty } from "@nestjs/swagger";
3+
import {
4+
IsString,
5+
IsOptional,
6+
IsEmail,
7+
IsArray,
8+
ValidateNested,
9+
IsBoolean,
10+
IsDate,
11+
} from "class-validator";
12+
import { Type } from "class-transformer";
13+
import { ObjectId } from "mongodb";
14+
15+
export class DowngradeUserDto {
16+
@ApiProperty({ example: "64f03af32e420f7f68055b92" })
17+
@IsMongoId()
18+
@IsNotEmpty()
19+
teamId: string;
20+
21+
@ApiProperty({ example: "64f03af32e420f7f68055b92" })
22+
@IsMongoId()
23+
@IsNotEmpty()
24+
userId: string;
25+
}
26+
27+
export class TourGuideDto {
28+
@IsBoolean()
29+
@IsOptional()
30+
isRequestTestsNoCodeDemoCompleted?: boolean;
31+
32+
@IsBoolean()
33+
@IsOptional()
34+
isGenerateVariableDemoCompleted?: boolean;
35+
36+
@IsBoolean()
37+
@IsOptional()
38+
isRequestTestsScriptDemoCompleted?: boolean;
39+
}
40+
41+
class UserWorkspaceDto {
42+
@IsMongoId()
43+
@IsNotEmpty()
44+
workspaceId: string;
45+
46+
@IsString()
47+
@IsNotEmpty()
48+
name: string;
49+
50+
@IsMongoId()
51+
@IsNotEmpty()
52+
teamId: string;
53+
54+
@IsBoolean()
55+
@IsOptional()
56+
isNewInvite?: boolean;
57+
}
58+
59+
class TeamDto {
60+
@IsMongoId()
61+
@IsNotEmpty()
62+
id: ObjectId;
63+
64+
@IsString()
65+
@IsNotEmpty()
66+
name: string;
67+
68+
@IsString()
69+
@IsNotEmpty()
70+
role: string;
71+
72+
@IsBoolean()
73+
@IsOptional()
74+
isNewInvite?: boolean;
75+
76+
@IsOptional()
77+
@IsDate()
78+
joinedAt?: Date;
79+
}
80+
81+
class DownGradeUserDto {
82+
@IsString()
83+
@IsOptional()
84+
name?: string;
85+
86+
@IsEmail()
87+
@IsOptional()
88+
email?: string;
89+
90+
@IsArray()
91+
@IsOptional()
92+
@Type(() => TeamDto)
93+
teams?: TeamDto[];
94+
95+
@IsArray()
96+
@Type(() => UserWorkspaceDto)
97+
@IsOptional()
98+
@ValidateNested({ each: true })
99+
workspaces?: UserWorkspaceDto[];
100+
}
101+
102+
class DownGradeUserGenerateVariableDto extends DownGradeUserDto {
103+
@IsArray()
104+
@IsString({ each: true })
105+
@Type(() => String)
106+
isGenerateVariableTrial?: string[];
107+
}
108+
109+
export class DownGradeUserTourGuideDto extends DownGradeUserGenerateVariableDto {
110+
@IsOptional()
111+
@ValidateNested()
112+
@Type(() => TourGuideDto)
113+
tourGuide?: TourGuideDto;
114+
}

src/modules/billing/payloads/stripe.payload.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
2+
import {
3+
downgradeUser,
4+
downgradeWorkspace,
5+
} from "@src/modules/common/models/billing.model";
26
import {
37
IsEmail,
48
IsString,
@@ -7,6 +11,8 @@ import {
711
IsBoolean,
812
IsNotEmpty,
913
IsNumber,
14+
IsArray,
15+
ValidateNested,
1016
} from "class-validator";
1117

1218
export class CreateCustomerDto {
@@ -199,6 +205,28 @@ export class UpdateSubscriptionDto {
199205
@IsNumber()
200206
seats?: number;
201207
paymentBehavior?: "default_incomplete" | "allow_incomplete";
208+
209+
@ApiPropertyOptional({
210+
description: "List of workspaces to downgrade",
211+
example: [
212+
{ id: "workspace-id-1", name: "Workspace One" },
213+
{ id: "workspace-id-2", name: "Workspace Two" },
214+
],
215+
})
216+
@IsOptional()
217+
@IsArray()
218+
workspaces?: downgradeWorkspace[];
219+
220+
@ApiPropertyOptional({
221+
description: "List of users to downgrade",
222+
example: [
223+
{ id: "user-id-1", email: "[email protected]" },
224+
{ id: "user-id-2", email: "[email protected]" },
225+
],
226+
})
227+
@IsOptional()
228+
@IsArray()
229+
users?: downgradeUser[];
202230
}
203231

204232
export class CancelSubscriptionDto {
@@ -210,6 +238,26 @@ export class CancelSubscriptionDto {
210238
@IsOptional()
211239
@IsBoolean()
212240
cancelImmediately?: boolean;
241+
242+
@ApiPropertyOptional({
243+
description: "team Id",
244+
example: "688731d965594536e3ffdce1",
245+
})
246+
@IsOptional()
247+
@IsString()
248+
teamId?: string;
249+
250+
@IsOptional()
251+
@IsArray()
252+
workspaces?: downgradeWorkspace[];
253+
254+
@ApiPropertyOptional({
255+
description: "List of user IDs to downgrade",
256+
example: ["[email protected]"],
257+
})
258+
@IsOptional()
259+
@IsArray()
260+
users?: downgradeUser[];
213261
}
214262

215263
export class ReactivateSubscriptionDto {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Injectable, Inject } from "@nestjs/common";
2+
import { Collections } from "@src/modules/common/enum/database.collection.enum";
3+
import { Team, TeamDto } from "@src/modules/common/models/team.model";
4+
import { Db } from "mongodb";
5+
import { ObjectId, WithId } from "mongodb";
6+
7+
/**
8+
* Repository for handling promo code database operations
9+
*/
10+
@Injectable()
11+
export class DownGradeTeamRepository {
12+
constructor(@Inject("DATABASE_CONNECTION") private db: Db) {}
13+
14+
async findTeamByTeamId(id: ObjectId): Promise<WithId<Team>> {
15+
const teamData = await this.db
16+
.collection<Team>(Collections.TEAM)
17+
.findOne({ _id: id });
18+
return teamData;
19+
}
20+
21+
async updateTeamById(
22+
id: ObjectId,
23+
updateParams: Partial<any>,
24+
): Promise<WithId<Team>> {
25+
const updatedTeamParams = {
26+
$set: updateParams,
27+
};
28+
const responseData = await this.db
29+
.collection<Team>(Collections.TEAM)
30+
.findOneAndUpdate({ _id: id }, updatedTeamParams);
31+
return responseData.value;
32+
}
33+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Injectable, Inject } from "@nestjs/common";
2+
import { Db } from "mongodb";
3+
import { ObjectId, WithId } from "mongodb";
4+
import { User } from "@src/modules/common/models/user.model";
5+
import { Collections } from "@src/modules/common/enum/database.collection.enum";
6+
import { DownGradeUserTourGuideDto } from "../payloads/downgrade-user.payload";
7+
8+
/**
9+
* Repository for handling promo code database operations
10+
*/
11+
@Injectable()
12+
export class DownGradeUserRepository {
13+
constructor(@Inject("DATABASE_CONNECTION") private db: Db) {}
14+
async findUserByUserId(id: ObjectId): Promise<WithId<User>> {
15+
const userData = await this.db
16+
.collection<User>(Collections.USER)
17+
.findOne({ _id: id });
18+
return userData;
19+
}
20+
21+
async updateUserById(
22+
id: ObjectId,
23+
updateParams: Partial<DownGradeUserTourGuideDto>,
24+
): Promise<WithId<User>> {
25+
const updatedUserParams = {
26+
$set: updateParams,
27+
};
28+
const responseData = await this.db
29+
.collection<User>(Collections.USER)
30+
.findOneAndUpdate({ _id: id }, updatedUserParams);
31+
return responseData.value;
32+
}
33+
34+
async findUsersByIdArray(IdArray: Array<ObjectId>): Promise<WithId<User>[]> {
35+
const response = await this.db
36+
.collection<User>(Collections.USER)
37+
.find({ _id: { $in: IdArray } })
38+
.toArray();
39+
return response;
40+
}
41+
}

0 commit comments

Comments
 (0)