5188 bug some canceled subscriptions are billed (#5254)

When user is deleting its account on a specific workspace, we remove it
as if it was a workspaceMember, and if no workspaceMember remains, we
delete the workspace and the associated stripe subscription
This commit is contained in:
martmull
2024-05-13 10:23:32 +02:00
committed by GitHub
parent 92acfe57a1
commit 1ac8abb118
11 changed files with 157 additions and 101 deletions

View File

@ -1,10 +1,12 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { User } from 'src/engine/core-modules/user/user.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
import { UserService } from './user.service';
@ -31,6 +33,14 @@ describe('UserService', () => {
provide: TypeORMService,
useValue: {},
},
{
provide: EventEmitter2,
useValue: {},
},
{
provide: WorkspaceService,
useValue: {},
},
],
}).compile();

View File

@ -1,4 +1,5 @@
import { InjectRepository } from '@nestjs/typeorm';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { Repository } from 'typeorm';
@ -8,17 +9,20 @@ import { User } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { WorkspaceMemberObjectMetadata } from 'src/modules/workspace-member/standard-objects/workspace-member.object-metadata';
import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event';
import { ObjectRecord } from 'src/engine/workspace-manager/workspace-sync-metadata/types/object-record';
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
export class UserService extends TypeOrmQueryService<User> {
constructor(
@InjectRepository(User, 'core')
private readonly userRepository: Repository<User>,
@InjectRepository(UserWorkspace, 'core')
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
private readonly dataSourceService: DataSourceService,
private readonly typeORMService: TypeORMService,
private readonly eventEmitter: EventEmitter2,
private readonly workspaceService: WorkspaceService,
) {
super(userRepository);
}
@ -95,64 +99,46 @@ export class UserService extends TypeOrmQueryService<User> {
assert(user, 'User not found');
const workspaceId = user.defaultWorkspaceId;
const dataSourceMetadata =
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
user.defaultWorkspace.id,
workspaceId,
);
const workspaceDataSource =
await this.typeORMService.connectToDataSource(dataSourceMetadata);
const workspaceMembers = await workspaceDataSource?.query(
`SELECT * FROM ${dataSourceMetadata.schema}."workspaceMember"`,
);
const workspaceMember = workspaceMembers.filter(
(member: ObjectRecord<WorkspaceMemberObjectMetadata>) =>
member.userId === userId,
)?.[0];
assert(workspaceMember, 'WorkspaceMember not found');
if (workspaceMembers.length === 1) {
await this.workspaceService.deleteWorkspace(workspaceId);
return user;
}
await workspaceDataSource?.query(
`DELETE FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "userId" = '${userId}'`,
);
const payload =
new ObjectRecordDeleteEvent<WorkspaceMemberObjectMetadata>();
await this.userWorkspaceRepository.delete({ userId });
payload.workspaceId = workspaceId;
payload.properties = {
before: workspaceMember,
};
payload.recordId = workspaceMember.id;
await this.userRepository.delete(user.id);
this.eventEmitter.emit('workspaceMember.deleted', payload);
return user;
}
async handleRemoveWorkspaceMember(workspaceId: string, userId: string) {
await this.userWorkspaceRepository.delete({
userId,
workspaceId,
});
await this.reassignOrRemoveUserDefaultWorkspace(workspaceId, userId);
}
private async reassignOrRemoveUserDefaultWorkspace(
workspaceId: string,
userId: string,
) {
const userWorkspaces = await this.userWorkspaceRepository.find({
where: { userId: userId },
});
if (userWorkspaces.length === 0) {
await this.userRepository.delete({ id: userId });
return;
}
const user = await this.userRepository.findOne({
where: {
id: userId,
},
});
if (!user) {
throw new Error(`User ${userId} not found in workspace ${workspaceId}`);
}
if (user.defaultWorkspaceId === workspaceId) {
await this.userRepository.update(
{ id: userId },
{
defaultWorkspaceId: userWorkspaces[0].workspaceId,
},
);
}
}
}

View File

@ -9,9 +9,8 @@ import { UserResolver } from 'src/engine/core-modules/user/user.resolver';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
import { userAutoResolverOpts } from './user.auto-resolver-opts';
@ -21,14 +20,14 @@ import { UserService } from './services/user.service';
imports: [
NestjsQueryGraphQLModule.forFeature({
imports: [
NestjsQueryTypeOrmModule.forFeature([User, UserWorkspace], 'core'),
NestjsQueryTypeOrmModule.forFeature([User], 'core'),
TypeORMModule,
],
resolvers: userAutoResolverOpts,
}),
DataSourceModule,
FileUploadModule,
UserWorkspaceModule,
WorkspaceModule,
],
exports: [UserService],
providers: [UserService, UserResolver, TypeORMService],