From 1b3181b14e0a96b8454a2ffb1f91033be5e20778 Mon Sep 17 00:00:00 2001 From: Etienne <45695613+etiennejouan@users.noreply.github.com> Date: Wed, 29 Jan 2025 19:30:46 +0100 Subject: [PATCH] add command to run cleaning suspended workspaces job (#9895) Co-authored-by: etiennejouan --- .../core-modules/message-queue/jobs.module.ts | 2 + .../clean-suspended-workspaces.command.ts | 33 +++ .../crons/clean-suspended-workspaces.job.ts | 278 +----------------- .../exceptions/workspace-cleaner.exception.ts | 12 + .../services/cleaner.workspace-service.ts | 267 +++++++++++++++++ .../workspace-cleaner.module.ts | 21 +- 6 files changed, 344 insertions(+), 269 deletions(-) create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/clean-suspended-workspaces.command.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/exceptions/workspace-cleaner.exception.ts create mode 100644 packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts diff --git a/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts b/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts index df1afd094151..5b071a0cd020 100644 --- a/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts +++ b/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts @@ -22,6 +22,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; import { CleanSuspendedWorkspacesJob } from 'src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.job'; import { CleanWorkspaceDeletionWarningUserVarsJob } from 'src/engine/workspace-manager/workspace-cleaner/jobs/clean-workspace-deletion-warning-user-vars.job'; +import { WorkspaceCleanerModule } from 'src/engine/workspace-manager/workspace-cleaner/workspace-cleaner.module'; import { CalendarEventParticipantManagerModule } from 'src/modules/calendar/calendar-event-participant-manager/calendar-event-participant-manager.module'; import { CalendarModule } from 'src/modules/calendar/calendar.module'; import { AutoCompaniesAndContactsCreationJobModule } from 'src/modules/contact-creation-manager/jobs/auto-companies-and-contacts-creation-job.module'; @@ -56,6 +57,7 @@ import { WorkflowModule } from 'src/modules/workflow/workflow.module'; WebhookJobModule, WorkflowModule, FavoriteModule, + WorkspaceCleanerModule, ], providers: [ CleanSuspendedWorkspacesJob, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/clean-suspended-workspaces.command.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/clean-suspended-workspaces.command.ts new file mode 100644 index 000000000000..e3842f37725f --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/clean-suspended-workspaces.command.ts @@ -0,0 +1,33 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { Repository } from 'typeorm'; + +import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command'; +import { BaseCommandOptions } from 'src/database/commands/base.command'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { CleanerWorkspaceService } from 'src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service'; + +@Command({ + name: 'workspace:clean', + description: 'Clean suspended workspace', +}) +export class CleanSuspendedWorkspacesCommand extends ActiveWorkspacesCommandRunner { + constructor( + private readonly cleanerWorkspaceService: CleanerWorkspaceService, + @InjectRepository(Workspace, 'core') + protected readonly workspaceRepository: Repository, + ) { + super(workspaceRepository); + } + + async executeActiveWorkspacesCommand( + _passedParam: string[], + _options: BaseCommandOptions, + workspaceIds: string[], + ): Promise { + await this.cleanerWorkspaceService.batchWarnOrCleanSuspendedWorkspaces( + workspaceIds, + ); + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.job.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.job.ts index f5c756997a1c..8a9da6e18db3 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.job.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/crons/clean-suspended-workspaces.job.ts @@ -1,289 +1,33 @@ -import { Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { render } from '@react-email/components'; -import chunk from 'lodash.chunk'; -import { - CleanSuspendedWorkspaceEmail, - WarnSuspendedWorkspaceEmail, -} from 'twenty-emails'; import { WorkspaceActivationStatus } from 'twenty-shared'; import { Repository } from 'typeorm'; -import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; -import { EmailService } from 'src/engine/core-modules/email/email.service'; -import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { UserService } from 'src/engine/core-modules/user/services/user.service'; -import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service'; -import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { USER_WORKSPACE_DELETION_WARNING_SENT_KEY } from 'src/engine/workspace-manager/workspace-cleaner/constants/user-workspace-deletion-warning-sent-key.constant'; -import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; - -const MILLISECONDS_IN_ONE_DAY = 1000 * 3600 * 24; +import { CleanerWorkspaceService } from 'src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service'; @Processor(MessageQueue.cronQueue) export class CleanSuspendedWorkspacesJob { - private readonly logger = new Logger(CleanSuspendedWorkspacesJob.name); - private readonly inactiveDaysBeforeDelete: number; - private readonly inactiveDaysBeforeWarn: number; - private readonly maxNumberOfWorkspacesDeletedPerExecution: number; - constructor( - private readonly workspaceService: WorkspaceService, - private readonly environmentService: EnvironmentService, - private readonly userService: UserService, - private readonly userVarsService: UserVarsService, - private readonly emailService: EmailService, - @InjectRepository(BillingSubscription, 'core') - private readonly billingSubscriptionRepository: Repository, + private readonly cleanerWorkspaceService: CleanerWorkspaceService, @InjectRepository(Workspace, 'core') private readonly workspaceRepository: Repository, - ) { - this.inactiveDaysBeforeDelete = this.environmentService.get( - 'WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION', - ); - this.inactiveDaysBeforeWarn = this.environmentService.get( - 'WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION', - ); - this.maxNumberOfWorkspacesDeletedPerExecution = this.environmentService.get( - 'MAX_NUMBER_OF_WORKSPACES_DELETED_PER_EXECUTION', - ); - } - - async computeWorkspaceBillingInactivity( - workspace: Workspace, - ): Promise { - try { - const lastSubscription = - await this.billingSubscriptionRepository.findOneOrFail({ - where: { workspaceId: workspace.id }, - order: { updatedAt: 'DESC' }, - }); - - const daysSinceBillingInactivity = Math.floor( - (new Date().getTime() - lastSubscription.updatedAt.getTime()) / - MILLISECONDS_IN_ONE_DAY, - ); - - return daysSinceBillingInactivity; - } catch { - this.logger.error( - `No billing subscription found for workspace ${workspace.id} ${workspace.displayName}`, - ); - - return null; - } - } - - async checkIfWorkspaceMembersWarned( - workspaceMembers: WorkspaceMemberWorkspaceEntity[], - workspaceId: string, - ) { - for (const workspaceMember of workspaceMembers) { - const workspaceMemberWarned = - (await this.userVarsService.get({ - userId: workspaceMember.userId, - workspaceId: workspaceId, - key: USER_WORKSPACE_DELETION_WARNING_SENT_KEY, - })) === true; - - if (workspaceMemberWarned) { - return true; - } - } - - return false; - } - - async sendWarningEmail( - workspaceMember: WorkspaceMemberWorkspaceEntity, - workspaceDisplayName: string | undefined, - daysSinceInactive: number, - ) { - const emailData = { - daysSinceInactive, - inactiveDaysBeforeDelete: this.inactiveDaysBeforeDelete, - userName: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`, - workspaceDisplayName: `${workspaceDisplayName}`, - }; - const emailTemplate = WarnSuspendedWorkspaceEmail(emailData); - const html = render(emailTemplate, { - pretty: true, - }); - const text = render(emailTemplate, { - plainText: true, - }); - - this.emailService.send({ - to: workspaceMember.userEmail, - bcc: this.environmentService.get('EMAIL_SYSTEM_ADDRESS'), - from: `${this.environmentService.get( - 'EMAIL_FROM_NAME', - )} <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`, - subject: 'Action needed to prevent workspace deletion', - html, - text, - }); - } - - async warnWorkspaceMembers(workspace: Workspace, daysSinceInactive: number) { - const workspaceMembers = - await this.userService.loadWorkspaceMembers(workspace); - - const workspaceMembersWarned = await this.checkIfWorkspaceMembersWarned( - workspaceMembers, - workspace.id, - ); - - if (workspaceMembersWarned) { - this.logger.log( - `Workspace ${workspace.id} ${workspace.displayName} already warned`, - ); - - return; - } else { - this.logger.log( - `Sending ${workspace.id} ${ - workspace.displayName - } suspended since ${daysSinceInactive} days emails to users ['${workspaceMembers - .map((workspaceUser) => workspaceUser.userEmail) - .join(', ')}']`, - ); - - const workspaceMembersChunks = chunk(workspaceMembers, 5); - - for (const workspaceMembersChunk of workspaceMembersChunks) { - await Promise.all( - workspaceMembersChunk.map(async (workspaceMember) => { - await this.userVarsService.set({ - userId: workspaceMember.userId, - workspaceId: workspace.id, - key: USER_WORKSPACE_DELETION_WARNING_SENT_KEY, - value: true, - }); - - await this.sendWarningEmail( - workspaceMember, - workspace.displayName, - daysSinceInactive, - ); - }), - ); - } - - return; - } - } - - async sendCleaningEmail( - workspaceMember: WorkspaceMemberWorkspaceEntity, - workspaceDisplayName: string | undefined, - ) { - const emailData = { - inactiveDaysBeforeDelete: this.inactiveDaysBeforeDelete, - userName: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`, - workspaceDisplayName: `${workspaceDisplayName}`, - }; - const emailTemplate = CleanSuspendedWorkspaceEmail(emailData); - const html = render(emailTemplate, { - pretty: true, - }); - const text = render(emailTemplate, { - plainText: true, - }); - - this.emailService.send({ - to: workspaceMember.userEmail, - bcc: this.environmentService.get('EMAIL_SYSTEM_ADDRESS'), - from: `${this.environmentService.get( - 'EMAIL_FROM_NAME', - )} <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`, - subject: 'Your workspace has been deleted', - html, - text, - }); - } - - async informWorkspaceMembersAndDeleteWorkspace(workspace: Workspace) { - const workspaceMembers = - await this.userService.loadWorkspaceMembers(workspace); - - this.logger.log( - `Sending workspace ${workspace.id} ${ - workspace.displayName - } deletion emails to users ['${workspaceMembers - .map((workspaceUser) => workspaceUser.userEmail) - .join(', ')}']`, - ); - - const workspaceMembersChunks = chunk(workspaceMembers, 5); - - for (const workspaceMembersChunk of workspaceMembersChunks) { - await Promise.all( - workspaceMembersChunk.map(async (workspaceMember) => { - await this.userVarsService.delete({ - userId: workspaceMember.userId, - workspaceId: workspace.id, - key: USER_WORKSPACE_DELETION_WARNING_SENT_KEY, - }); - - await this.sendCleaningEmail(workspaceMember, workspace.displayName); - }), - ); - } - - await this.workspaceService.deleteWorkspace(workspace.id); - this.logger.log( - `Cleaning Workspace ${workspace.id} ${workspace.displayName}`, - ); - } + ) {} @Process(CleanSuspendedWorkspacesJob.name) async handle(): Promise { - this.logger.log(`Job running...`); - - const suspendedWorkspaces = await this.workspaceRepository.find({ - where: { activationStatus: WorkspaceActivationStatus.SUSPENDED }, + const suspendedWorkspaceIds = await this.workspaceRepository.find({ + select: ['id'], + where: { + activationStatus: WorkspaceActivationStatus.SUSPENDED, + }, }); - const suspendedWorkspacesChunks = chunk(suspendedWorkspaces, 5); - - let deletedWorkspacesCount = 0; - - for (const suspendedWorkspacesChunk of suspendedWorkspacesChunks) { - await Promise.all( - suspendedWorkspacesChunk.map(async (workspace) => { - const workspaceInactivity = - await this.computeWorkspaceBillingInactivity(workspace); - - if ( - workspaceInactivity && - workspaceInactivity > this.inactiveDaysBeforeDelete && - deletedWorkspacesCount <= - this.maxNumberOfWorkspacesDeletedPerExecution - ) { - await this.informWorkspaceMembersAndDeleteWorkspace(workspace); - deletedWorkspacesCount++; - - return; - } - if ( - workspaceInactivity && - workspaceInactivity > this.inactiveDaysBeforeWarn && - workspaceInactivity <= this.inactiveDaysBeforeDelete - ) { - await this.warnWorkspaceMembers(workspace, workspaceInactivity); - - return; - } - }), - ); - } - - this.logger.log(`Job done!`); + await this.cleanerWorkspaceService.batchWarnOrCleanSuspendedWorkspaces( + suspendedWorkspaceIds.map((workspace) => workspace.id), + ); } } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/exceptions/workspace-cleaner.exception.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/exceptions/workspace-cleaner.exception.ts new file mode 100644 index 000000000000..99c185b088d9 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/exceptions/workspace-cleaner.exception.ts @@ -0,0 +1,12 @@ +import { CustomException } from 'src/utils/custom-exception'; + +export class WorkspaceCleanerException extends CustomException { + code: WorkspaceCleanerExceptionCode; + constructor(message: string, code: WorkspaceCleanerExceptionCode) { + super(message, code); + } +} + +export enum WorkspaceCleanerExceptionCode { + BILLING_SUBSCRIPTION_NOT_FOUND = 'BILLING_SUBSCRIPTION_NOT_FOUND', +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts new file mode 100644 index 000000000000..270c511495e1 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts @@ -0,0 +1,267 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { render } from '@react-email/components'; +import { + CleanSuspendedWorkspaceEmail, + WarnSuspendedWorkspaceEmail, +} from 'twenty-emails'; +import { WorkspaceActivationStatus } from 'twenty-shared'; +import { In, Repository } from 'typeorm'; + +import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; +import { EmailService } from 'src/engine/core-modules/email/email.service'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +import { UserService } from 'src/engine/core-modules/user/services/user.service'; +import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service'; +import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { USER_WORKSPACE_DELETION_WARNING_SENT_KEY } from 'src/engine/workspace-manager/workspace-cleaner/constants/user-workspace-deletion-warning-sent-key.constant'; +import { + WorkspaceCleanerException, + WorkspaceCleanerExceptionCode, +} from 'src/engine/workspace-manager/workspace-cleaner/exceptions/workspace-cleaner.exception'; +import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; + +const MILLISECONDS_IN_ONE_DAY = 1000 * 3600 * 24; + +@Injectable() +export class CleanerWorkspaceService { + private readonly logger = new Logger(CleanerWorkspaceService.name); + private readonly inactiveDaysBeforeDelete: number; + private readonly inactiveDaysBeforeWarn: number; + private readonly maxNumberOfWorkspacesDeletedPerExecution: number; + + constructor( + private readonly workspaceService: WorkspaceService, + private readonly environmentService: EnvironmentService, + private readonly userVarsService: UserVarsService, + private readonly userService: UserService, + private readonly emailService: EmailService, + @InjectRepository(Workspace, 'core') + private readonly workspaceRepository: Repository, + @InjectRepository(BillingSubscription, 'core') + private readonly billingSubscriptionRepository: Repository, + ) { + this.inactiveDaysBeforeDelete = this.environmentService.get( + 'WORKSPACE_INACTIVE_DAYS_BEFORE_DELETION', + ); + this.inactiveDaysBeforeWarn = this.environmentService.get( + 'WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION', + ); + this.maxNumberOfWorkspacesDeletedPerExecution = this.environmentService.get( + 'MAX_NUMBER_OF_WORKSPACES_DELETED_PER_EXECUTION', + ); + } + + async computeWorkspaceBillingInactivity( + workspace: Workspace, + ): Promise { + try { + const lastSubscription = + await this.billingSubscriptionRepository.findOneOrFail({ + where: { workspaceId: workspace.id }, + order: { updatedAt: 'DESC' }, + }); + + const daysSinceBillingInactivity = Math.floor( + (new Date().getTime() - lastSubscription.updatedAt.getTime()) / + MILLISECONDS_IN_ONE_DAY, + ); + + return daysSinceBillingInactivity; + } catch { + throw new WorkspaceCleanerException( + `No billing subscription found for workspace ${workspace.id} ${workspace.displayName}`, + WorkspaceCleanerExceptionCode.BILLING_SUBSCRIPTION_NOT_FOUND, + ); + } + } + + async checkIfAtLeastOneWorkspaceMemberWarned( + workspaceMembers: WorkspaceMemberWorkspaceEntity[], + workspaceId: string, + ) { + for (const workspaceMember of workspaceMembers) { + const workspaceMemberWarned = await this.userVarsService.get({ + userId: workspaceMember.userId, + workspaceId: workspaceId, + key: USER_WORKSPACE_DELETION_WARNING_SENT_KEY, + }); + + if (workspaceMemberWarned) { + return true; + } + } + + return false; + } + + async sendWarningEmail( + workspaceMember: WorkspaceMemberWorkspaceEntity, + workspaceDisplayName: string | undefined, + daysSinceInactive: number, + ) { + const emailData = { + daysSinceInactive, + inactiveDaysBeforeDelete: this.inactiveDaysBeforeDelete, + userName: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`, + workspaceDisplayName: `${workspaceDisplayName}`, + }; + const emailTemplate = WarnSuspendedWorkspaceEmail(emailData); + const html = render(emailTemplate, { pretty: true }); + const text = render(emailTemplate, { plainText: true }); + + this.emailService.send({ + to: workspaceMember.userEmail, + bcc: this.environmentService.get('EMAIL_SYSTEM_ADDRESS'), + from: `${this.environmentService.get( + 'EMAIL_FROM_NAME', + )} <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`, + subject: 'Action needed to prevent workspace deletion', + html, + text, + }); + } + + async warnWorkspaceMembers(workspace: Workspace, daysSinceInactive: number) { + const workspaceMembers = + await this.userService.loadWorkspaceMembers(workspace); + + const workspaceMembersWarned = + await this.checkIfAtLeastOneWorkspaceMemberWarned( + workspaceMembers, + workspace.id, + ); + + if (workspaceMembersWarned) { + this.logger.log( + `Workspace ${workspace.id} ${workspace.displayName} already warned`, + ); + + return; + } + + this.logger.log( + `Sending ${workspace.id} ${ + workspace.displayName + } suspended since ${daysSinceInactive} days emails to users ['${workspaceMembers + .map((workspaceUser) => workspaceUser.id) + .join(', ')}']`, + ); + + for (const workspaceMember of workspaceMembers) { + await this.userVarsService.set({ + userId: workspaceMember.userId, + workspaceId: workspace.id, + key: USER_WORKSPACE_DELETION_WARNING_SENT_KEY, + value: true, + }); + + await this.sendWarningEmail( + workspaceMember, + workspace.displayName, + daysSinceInactive, + ); + } + } + + async sendCleaningEmail( + workspaceMember: WorkspaceMemberWorkspaceEntity, + workspaceDisplayName: string, + ) { + const emailData = { + inactiveDaysBeforeDelete: this.inactiveDaysBeforeDelete, + userName: `${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`, + workspaceDisplayName, + }; + const emailTemplate = CleanSuspendedWorkspaceEmail(emailData); + const html = render(emailTemplate, { + pretty: true, + }); + const text = render(emailTemplate, { + plainText: true, + }); + + this.emailService.send({ + to: workspaceMember.userEmail, + bcc: this.environmentService.get('EMAIL_SYSTEM_ADDRESS'), + from: `${this.environmentService.get( + 'EMAIL_FROM_NAME', + )} <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`, + subject: 'Your workspace has been deleted', + html, + text, + }); + } + + async informWorkspaceMembersAndDeleteWorkspace(workspace: Workspace) { + const workspaceMembers = + await this.userService.loadWorkspaceMembers(workspace); + + this.logger.log( + `Sending workspace ${workspace.id} ${ + workspace.displayName + } deletion emails to users ['${workspaceMembers + .map((workspaceUser) => workspaceUser.id) + .join(', ')}']`, + ); + + for (const workspaceMember of workspaceMembers) { + await this.userVarsService.delete({ + userId: workspaceMember.userId, + workspaceId: workspace.id, + key: USER_WORKSPACE_DELETION_WARNING_SENT_KEY, + }); + + await this.sendCleaningEmail( + workspaceMember, + workspace.displayName || '', + ); + } + + await this.workspaceService.deleteWorkspace(workspace.id); + this.logger.log( + `Cleaning Workspace ${workspace.id} ${workspace.displayName}`, + ); + } + + async batchWarnOrCleanSuspendedWorkspaces( + workspaceIds: string[], + ): Promise { + this.logger.log(`batchWarnOrCleanSuspendedWorkspaces running...`); + + const workspaces = await this.workspaceRepository.find({ + where: { + id: In(workspaceIds), + activationStatus: WorkspaceActivationStatus.SUSPENDED, + }, + }); + + let deletedWorkspacesCount = 0; + + for (const workspace of workspaces) { + const workspaceInactivity = + await this.computeWorkspaceBillingInactivity(workspace); + + if ( + workspaceInactivity && + workspaceInactivity > this.inactiveDaysBeforeDelete && + deletedWorkspacesCount <= this.maxNumberOfWorkspacesDeletedPerExecution + ) { + await this.informWorkspaceMembersAndDeleteWorkspace(workspace); + deletedWorkspacesCount++; + + continue; + } + if ( + workspaceInactivity && + workspaceInactivity > this.inactiveDaysBeforeWarn && + workspaceInactivity <= this.inactiveDaysBeforeDelete + ) { + await this.warnWorkspaceMembers(workspace, workspaceInactivity); + } + } + this.logger.log(`batchWarnOrCleanSuspendedWorkspaces done!`); + } +} diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/workspace-cleaner.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/workspace-cleaner.module.ts index fa18ef9ff081..ee74d5e6bed4 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/workspace-cleaner.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/workspace-cleaner.module.ts @@ -1,18 +1,35 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; +import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; +import { EmailModule } from 'src/engine/core-modules/email/email.module'; +import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module'; +import { UserModule } from 'src/engine/core-modules/user/user.module'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; +import { CleanSuspendedWorkspacesCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/clean-suspended-workspaces.command'; import { CleanSuspendedWorkspacesCronCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/clean-suspended-workspaces.cron.command'; import { DeleteWorkspacesCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/delete-workspaces.command'; +import { CleanerWorkspaceService } from 'src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service'; @Module({ imports: [ - TypeOrmModule.forFeature([Workspace], 'core'), + TypeOrmModule.forFeature([Workspace, BillingSubscription], 'core'), WorkspaceModule, DataSourceModule, + UserVarsModule, + UserModule, + EmailModule, + BillingModule, ], - providers: [DeleteWorkspacesCommand, CleanSuspendedWorkspacesCronCommand], + providers: [ + DeleteWorkspacesCommand, + CleanSuspendedWorkspacesCommand, + CleanSuspendedWorkspacesCronCommand, + CleanerWorkspaceService, + ], + exports: [CleanerWorkspaceService], }) export class WorkspaceCleanerModule {}