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
This commit is contained in:
@ -4,34 +4,68 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { In, Repository } from 'typeorm';
|
import { In, Repository } from 'typeorm';
|
||||||
|
|
||||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
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 { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
import {
|
||||||
|
FeatureFlagEntity,
|
||||||
|
FeatureFlagKeys,
|
||||||
|
} from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BillingService {
|
export class BillingService {
|
||||||
protected readonly logger = new Logger(BillingService.name);
|
protected readonly logger = new Logger(BillingService.name);
|
||||||
constructor(
|
constructor(
|
||||||
private readonly environmentService: EnvironmentService,
|
private readonly environmentService: EnvironmentService,
|
||||||
|
@InjectRepository(BillingSubscription, 'core')
|
||||||
|
private readonly billingSubscriptionRepository: Repository<BillingSubscription>,
|
||||||
|
@InjectRepository(FeatureFlagEntity, 'core')
|
||||||
|
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
|
||||||
@InjectRepository(Workspace, 'core')
|
@InjectRepository(Workspace, 'core')
|
||||||
private readonly workspaceRepository: Repository<Workspace>,
|
private readonly workspaceRepository: Repository<Workspace>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async getActiveSubscriptionWorkspaceIds() {
|
async getActiveSubscriptionWorkspaceIds() {
|
||||||
return (
|
if (!this.environmentService.get('IS_BILLING_ENABLED')) {
|
||||||
await this.workspaceRepository.find({
|
return (await this.workspaceRepository.find({ select: ['id'] })).map(
|
||||||
where: this.environmentService.get('IS_BILLING_ENABLED')
|
(workspace) => workspace.id,
|
||||||
? {
|
);
|
||||||
currentBillingSubscription: {
|
}
|
||||||
status: In([
|
|
||||||
SubscriptionStatus.Active,
|
const activeSubscriptions = await this.billingSubscriptionRepository.find({
|
||||||
SubscriptionStatus.Trialing,
|
where: {
|
||||||
SubscriptionStatus.PastDue,
|
status: In([
|
||||||
]),
|
SubscriptionStatus.Active,
|
||||||
},
|
SubscriptionStatus.Trialing,
|
||||||
}
|
SubscriptionStatus.PastDue,
|
||||||
: {},
|
]),
|
||||||
select: ['id'],
|
},
|
||||||
})
|
select: ['workspaceId'],
|
||||||
).map((workspace) => workspace.id);
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -84,9 +84,6 @@ export class Workspace {
|
|||||||
@OneToMany(() => FeatureFlagEntity, (featureFlag) => featureFlag.workspace)
|
@OneToMany(() => FeatureFlagEntity, (featureFlag) => featureFlag.workspace)
|
||||||
featureFlags: Relation<FeatureFlagEntity[]>;
|
featureFlags: Relation<FeatureFlagEntity[]>;
|
||||||
|
|
||||||
@Field({ nullable: true })
|
|
||||||
currentBillingSubscription: BillingSubscription;
|
|
||||||
|
|
||||||
@Field({ nullable: true })
|
@Field({ nullable: true })
|
||||||
workspaceMembersCount: number;
|
workspaceMembersCount: number;
|
||||||
|
|
||||||
|
|||||||
@ -118,7 +118,7 @@ export class WorkspaceResolver {
|
|||||||
return this.workspaceCacheVersionService.getVersion(workspace.id);
|
return this.workspaceCacheVersionService.getVersion(workspace.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ResolveField(() => BillingSubscription)
|
@ResolveField(() => BillingSubscription, { nullable: true })
|
||||||
async currentBillingSubscription(
|
async currentBillingSubscription(
|
||||||
@Parent() workspace: Workspace,
|
@Parent() workspace: Workspace,
|
||||||
): Promise<BillingSubscription | null> {
|
): Promise<BillingSubscription | null> {
|
||||||
|
|||||||
@ -2,27 +2,29 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
|
|
||||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
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 { 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 { 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;
|
dryRun?: boolean;
|
||||||
workspaceIds?: string[];
|
workspaceIds: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@Command({
|
@Command({
|
||||||
name: 'workspace:delete-incomplete',
|
name: 'workspace:delete',
|
||||||
description: 'Delete incomplete workspaces',
|
description: 'Delete workspace',
|
||||||
})
|
})
|
||||||
export class DeleteIncompleteWorkspacesCommand extends CommandRunner {
|
export class DeleteWorkspacesCommand extends CommandRunner {
|
||||||
private readonly logger = new Logger(DeleteIncompleteWorkspacesCommand.name);
|
private readonly logger = new Logger(DeleteWorkspacesCommand.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly workspaceService: WorkspaceService,
|
private readonly workspaceService: WorkspaceService,
|
||||||
|
private readonly loadServiceWithWorkspaceContext: LoadServiceWithWorkspaceContext,
|
||||||
@InjectRepository(Workspace, 'core')
|
@InjectRepository(Workspace, 'core')
|
||||||
private readonly workspaceRepository: Repository<Workspace>,
|
private readonly workspaceRepository: Repository<Workspace>,
|
||||||
private readonly dataSourceService: DataSourceService,
|
private readonly dataSourceService: DataSourceService,
|
||||||
@ -42,7 +44,7 @@ export class DeleteIncompleteWorkspacesCommand extends CommandRunner {
|
|||||||
@Option({
|
@Option({
|
||||||
flags: '-w, --workspace-ids [workspace_ids]',
|
flags: '-w, --workspace-ids [workspace_ids]',
|
||||||
description: 'comma separated workspace ids',
|
description: 'comma separated workspace ids',
|
||||||
required: false,
|
required: true,
|
||||||
})
|
})
|
||||||
parseWorkspaceIds(value: string): string[] {
|
parseWorkspaceIds(value: string): string[] {
|
||||||
return value.split(',');
|
return value.split(',');
|
||||||
@ -50,41 +52,43 @@ export class DeleteIncompleteWorkspacesCommand extends CommandRunner {
|
|||||||
|
|
||||||
async run(
|
async run(
|
||||||
_passedParam: string[],
|
_passedParam: string[],
|
||||||
options: DeleteIncompleteWorkspacesCommandOptions,
|
options: DeleteWorkspacesCommandOptions,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const where: FindOptionsWhere<Workspace> = {
|
const workspaces = await this.workspaceRepository.find({
|
||||||
currentBillingSubscription: { status: SubscriptionStatus.Incomplete },
|
where: { id: In(options.workspaceIds) },
|
||||||
};
|
});
|
||||||
|
|
||||||
if (options.workspaceIds) {
|
|
||||||
where.id = In(options.workspaceIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
const incompleteWorkspaces = await this.workspaceRepository.findBy(where);
|
|
||||||
const dataSources =
|
const dataSources =
|
||||||
await this.dataSourceService.getManyDataSourceMetadata();
|
await this.dataSourceService.getManyDataSourceMetadata();
|
||||||
|
|
||||||
const workspaceIdsWithSchema = dataSources.map(
|
const workspaceIdsWithSchema = dataSources.map(
|
||||||
(dataSource) => dataSource.workspaceId,
|
(dataSource) => dataSource.workspaceId,
|
||||||
);
|
);
|
||||||
const incompleteWorkspacesToDelete = incompleteWorkspaces.filter(
|
|
||||||
(incompleteWorkspace) =>
|
const workspacesToDelete = workspaces.filter((Workspace) =>
|
||||||
workspaceIdsWithSchema.includes(incompleteWorkspace.id),
|
workspaceIdsWithSchema.includes(Workspace.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (incompleteWorkspacesToDelete.length) {
|
if (workspacesToDelete.length) {
|
||||||
this.logger.log(
|
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(
|
this.logger.log(
|
||||||
`${getDryRunLogHeader(options.dryRun)}Deleting workspace ${
|
`${getDryRunLogHeader(options.dryRun)}Deleting workspace ${
|
||||||
incompleteWorkspace.id
|
workspace.id
|
||||||
} name: '${incompleteWorkspace.displayName}'`,
|
} name: '${workspace.displayName}'`,
|
||||||
);
|
);
|
||||||
|
const workspaceServiceInstance =
|
||||||
|
await this.loadServiceWithWorkspaceContext.load(
|
||||||
|
this.workspaceService,
|
||||||
|
workspace.id,
|
||||||
|
);
|
||||||
|
|
||||||
if (!options.dryRun) {
|
if (!options.dryRun) {
|
||||||
await this.workspaceService.softDeleteWorkspace(incompleteWorkspace.id);
|
await workspaceServiceInstance.softDeleteWorkspace(workspace.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2,12 +2,12 @@ import { Module } from '@nestjs/common';
|
|||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
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 { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { CleanInactiveWorkspacesCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/clean-inactive-workspaces.command';
|
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 { 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 { 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 { 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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -16,7 +16,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s
|
|||||||
DataSourceModule,
|
DataSourceModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
DeleteIncompleteWorkspacesCommand,
|
DeleteWorkspacesCommand,
|
||||||
CleanInactiveWorkspacesCommand,
|
CleanInactiveWorkspacesCommand,
|
||||||
StartCleanInactiveWorkspacesCronCommand,
|
StartCleanInactiveWorkspacesCronCommand,
|
||||||
StopCleanInactiveWorkspacesCronCommand,
|
StopCleanInactiveWorkspacesCronCommand,
|
||||||
|
|||||||
Reference in New Issue
Block a user