Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
448 changes: 427 additions & 21 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions src/modules/billing/billing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -56,6 +57,7 @@ export class BillingModule {
DownGradeUserRepository,
DownGradeWorkspaceRepository,
DownGradeService,
ExcelEmailService,
BillingAuditService,
PromoCodeService,
PricingService,
Expand Down Expand Up @@ -85,6 +87,7 @@ export class BillingModule {
DownGradeUserRepository,
DownGradeWorkspaceRepository,
DownGradeService,
ExcelEmailService,
PricingService,
PaymentEmailService,
PaymentEmailHelper,
Expand Down
62 changes: 62 additions & 0 deletions src/modules/billing/helpers/payment-email.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<void> {
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<void> {
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
*/
Expand Down
5 changes: 3 additions & 2 deletions src/modules/billing/helpers/stripe-webhook.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,19 +95,20 @@ export class StripeWebhookHelper {
* Handle subscription updated webhook event
*/
private async handleSubscriptionUpdated(event: any): Promise<void> {
// 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
const teamUpdated = await this.stripeSubscriptionRepo.findTeamById(
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(
Expand Down
15 changes: 14 additions & 1 deletion src/modules/billing/payloads/downgrade-user.payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class DowngradeUserDto {
@ApiProperty({ example: "64f03af32e420f7f68055b92" })
@IsMongoId()
@IsNotEmpty()
userId: string;
userIds: string[];
}

export class TourGuideDto {
Expand Down Expand Up @@ -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;
}

57 changes: 55 additions & 2 deletions src/modules/billing/repositories/downgradeUser.repository.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
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";
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
Expand Down Expand Up @@ -31,11 +32,63 @@ export class DownGradeUserRepository {
return responseData.value;
}

async updateUserTeamsAndWorkspaces(
userId: ObjectId,
updatedTeams: TeamDto[],
updatedWorkspaces: UserWorkspaceDto[],
): Promise<WithId<User>> {
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<User> = {
teams: updatedTeams,
workspaces: updatedWorkspaces,
updatedAt: new Date(),
};
try {
const result = await this.db
.collection<User>(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<ObjectId>): Promise<WithId<User>[]> {
const response = await this.db
.collection<User>(Collections.USER)
.find({ _id: { $in: IdArray } })
.toArray();
return response;
}

async findUsersByStringIds(ids: string[]): Promise<WithId<User>[]> {
try {
const objectIds = ids.map((id) => new ObjectId(id));
const users = await this.db
.collection<User>(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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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 { AdminDto, UserDto, Workspace } from "@src/modules/common/models/workspace.model";
import { Db, DeleteResult } from "mongodb";
import { ObjectId, WithId } from "mongodb";

Expand All @@ -22,6 +22,26 @@ export class DownGradeWorkspaceRepository {
return data;
}

async getWorkspacesByIds(ids: string[]): Promise<WithId<Workspace>[]> {
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<Workspace>(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<DeleteResult> {
const _id = new ObjectId(id);
return this.db
Expand All @@ -46,4 +66,41 @@ export class DownGradeWorkspaceRepository {
}
return result.value;
}

async updateWorkspaceUsers(
workspaceId: string,
updatedUsers: UserDto[],
updatedAdmins?: AdminDto[],
): Promise<any> {
if (!workspaceId) {
throw new BadRequestException("workspaceId is required.");
}
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<Workspace> = {
users: updatedUsers,
updatedAt: new Date(),
};
if (updatedAdmins) {
updatePayload.admins = updatedAdmins;
}
try {
const result = await this.db
.collection<Workspace>(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/admins:", error);
}
}
}
Loading