From 88915291d9f1ade9cc1adbc689c248726e4728f6 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Mon, 1 Jul 2024 16:45:56 +0200 Subject: [PATCH] Fix Active Workspaces check (#6084) We have recently deprecated our subscriptionStatus on workspace to replace it by a check on existing subscription (+ freeAccess featureFlag) but the logic was not properly implemented --- .../core-modules/billing/billing.service.ts | 68 ++++++++++++++----- .../workspace/workspace.entity.ts | 3 - .../workspace/workspace.resolver.ts | 2 +- ...ommand.ts => delete-workspaces.command.ts} | 62 +++++++++-------- .../workspace-cleaner.module.ts | 4 +- 5 files changed, 87 insertions(+), 52 deletions(-) rename packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/{delete-incomplete-workspaces.command.ts => delete-workspaces.command.ts} (54%) diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.service.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.service.ts index d5cd471fe..34cae6a37 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.service.ts @@ -4,34 +4,68 @@ import { InjectRepository } from '@nestjs/typeorm'; import { In, Repository } from 'typeorm'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; -import { SubscriptionStatus } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; +import { + BillingSubscription, + SubscriptionStatus, +} from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { + FeatureFlagEntity, + FeatureFlagKeys, +} from 'src/engine/core-modules/feature-flag/feature-flag.entity'; @Injectable() export class BillingService { protected readonly logger = new Logger(BillingService.name); constructor( private readonly environmentService: EnvironmentService, + @InjectRepository(BillingSubscription, 'core') + private readonly billingSubscriptionRepository: Repository, + @InjectRepository(FeatureFlagEntity, 'core') + private readonly featureFlagRepository: Repository, @InjectRepository(Workspace, 'core') private readonly workspaceRepository: Repository, ) {} async getActiveSubscriptionWorkspaceIds() { - return ( - await this.workspaceRepository.find({ - where: this.environmentService.get('IS_BILLING_ENABLED') - ? { - currentBillingSubscription: { - status: In([ - SubscriptionStatus.Active, - SubscriptionStatus.Trialing, - SubscriptionStatus.PastDue, - ]), - }, - } - : {}, - select: ['id'], - }) - ).map((workspace) => workspace.id); + if (!this.environmentService.get('IS_BILLING_ENABLED')) { + return (await this.workspaceRepository.find({ select: ['id'] })).map( + (workspace) => workspace.id, + ); + } + + const activeSubscriptions = await this.billingSubscriptionRepository.find({ + where: { + status: In([ + SubscriptionStatus.Active, + SubscriptionStatus.Trialing, + SubscriptionStatus.PastDue, + ]), + }, + select: ['workspaceId'], + }); + + const freeAccessFeatureFlags = await this.featureFlagRepository.find({ + where: { + key: FeatureFlagKeys.IsFreeAccessEnabled, + value: true, + }, + select: ['workspaceId'], + }); + + const activeWorkspaceIdsBasedOnSubscriptions = activeSubscriptions.map( + (subscription) => subscription.workspaceId, + ); + + const activeWorkspaceIdsBasedOnFeatureFlags = freeAccessFeatureFlags.map( + (featureFlag) => featureFlag.workspaceId, + ); + + return Array.from( + new Set([ + ...activeWorkspaceIdsBasedOnSubscriptions, + ...activeWorkspaceIdsBasedOnFeatureFlags, + ]), + ); } } diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts index 3d8efa2b6..6b4d2b425 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts @@ -84,9 +84,6 @@ export class Workspace { @OneToMany(() => FeatureFlagEntity, (featureFlag) => featureFlag.workspace) featureFlags: Relation; - @Field({ nullable: true }) - currentBillingSubscription: BillingSubscription; - @Field({ nullable: true }) workspaceMembersCount: number; diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts index 9840cf110..5ead7800d 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts @@ -118,7 +118,7 @@ export class WorkspaceResolver { return this.workspaceCacheVersionService.getVersion(workspace.id); } - @ResolveField(() => BillingSubscription) + @ResolveField(() => BillingSubscription, { nullable: true }) async currentBillingSubscription( @Parent() workspace: Workspace, ): Promise { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/delete-incomplete-workspaces.command.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/delete-workspaces.command.ts similarity index 54% rename from packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/delete-incomplete-workspaces.command.ts rename to packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/delete-workspaces.command.ts index fb5faa313..2bbc17b75 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/delete-incomplete-workspaces.command.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/commands/delete-workspaces.command.ts @@ -2,27 +2,29 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Logger } from '@nestjs/common'; import { Command, CommandRunner, Option } from 'nest-commander'; -import { FindOptionsWhere, In, Repository } from 'typeorm'; +import { In, Repository } from 'typeorm'; -import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { getDryRunLogHeader } from 'src/utils/get-dry-run-log-header'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { SubscriptionStatus } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; +import { getDryRunLogHeader } from 'src/utils/get-dry-run-log-header'; +import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service'; +import { LoadServiceWithWorkspaceContext } from 'src/engine/twenty-orm/context/load-service-with-workspace.context'; -type DeleteIncompleteWorkspacesCommandOptions = { +type DeleteWorkspacesCommandOptions = { dryRun?: boolean; - workspaceIds?: string[]; + workspaceIds: string[]; }; @Command({ - name: 'workspace:delete-incomplete', - description: 'Delete incomplete workspaces', + name: 'workspace:delete', + description: 'Delete workspace', }) -export class DeleteIncompleteWorkspacesCommand extends CommandRunner { - private readonly logger = new Logger(DeleteIncompleteWorkspacesCommand.name); +export class DeleteWorkspacesCommand extends CommandRunner { + private readonly logger = new Logger(DeleteWorkspacesCommand.name); + constructor( private readonly workspaceService: WorkspaceService, + private readonly loadServiceWithWorkspaceContext: LoadServiceWithWorkspaceContext, @InjectRepository(Workspace, 'core') private readonly workspaceRepository: Repository, private readonly dataSourceService: DataSourceService, @@ -42,7 +44,7 @@ export class DeleteIncompleteWorkspacesCommand extends CommandRunner { @Option({ flags: '-w, --workspace-ids [workspace_ids]', description: 'comma separated workspace ids', - required: false, + required: true, }) parseWorkspaceIds(value: string): string[] { return value.split(','); @@ -50,41 +52,43 @@ export class DeleteIncompleteWorkspacesCommand extends CommandRunner { async run( _passedParam: string[], - options: DeleteIncompleteWorkspacesCommandOptions, + options: DeleteWorkspacesCommandOptions, ): Promise { - const where: FindOptionsWhere = { - currentBillingSubscription: { status: SubscriptionStatus.Incomplete }, - }; + const workspaces = await this.workspaceRepository.find({ + where: { id: In(options.workspaceIds) }, + }); - if (options.workspaceIds) { - where.id = In(options.workspaceIds); - } - - const incompleteWorkspaces = await this.workspaceRepository.findBy(where); const dataSources = await this.dataSourceService.getManyDataSourceMetadata(); + const workspaceIdsWithSchema = dataSources.map( (dataSource) => dataSource.workspaceId, ); - const incompleteWorkspacesToDelete = incompleteWorkspaces.filter( - (incompleteWorkspace) => - workspaceIdsWithSchema.includes(incompleteWorkspace.id), + + const workspacesToDelete = workspaces.filter((Workspace) => + workspaceIdsWithSchema.includes(Workspace.id), ); - if (incompleteWorkspacesToDelete.length) { + if (workspacesToDelete.length) { this.logger.log( - `Running Deleting incomplete workspaces on ${incompleteWorkspacesToDelete.length} workspaces`, + `Running Deleting workspaces on ${workspacesToDelete.length} workspaces`, ); } - for (const incompleteWorkspace of incompleteWorkspacesToDelete) { + for (const workspace of workspacesToDelete) { this.logger.log( `${getDryRunLogHeader(options.dryRun)}Deleting workspace ${ - incompleteWorkspace.id - } name: '${incompleteWorkspace.displayName}'`, + workspace.id + } name: '${workspace.displayName}'`, ); + const workspaceServiceInstance = + await this.loadServiceWithWorkspaceContext.load( + this.workspaceService, + workspace.id, + ); + if (!options.dryRun) { - await this.workspaceService.softDeleteWorkspace(incompleteWorkspace.id); + await workspaceServiceInstance.softDeleteWorkspace(workspace.id); } } } 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 6bf7eb013..681755be1 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 @@ -2,12 +2,12 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; -import { DeleteIncompleteWorkspacesCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/delete-incomplete-workspaces.command'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { CleanInactiveWorkspacesCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/clean-inactive-workspaces.command'; import { StartCleanInactiveWorkspacesCronCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/start-clean-inactive-workspaces.cron.command'; import { StopCleanInactiveWorkspacesCronCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/stop-clean-inactive-workspaces.cron.command'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; +import { DeleteWorkspacesCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/delete-workspaces.command'; @Module({ imports: [ @@ -16,7 +16,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s DataSourceModule, ], providers: [ - DeleteIncompleteWorkspacesCommand, + DeleteWorkspacesCommand, CleanInactiveWorkspacesCommand, StartCleanInactiveWorkspacesCronCommand, StopCleanInactiveWorkspacesCronCommand,