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 add42ae0b6a9..356cdec4e7fd 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 @@ -196,7 +196,10 @@ export class BillingService { ? frontBaseUrl + successUrlPath : frontBaseUrl; - let quantity = 1; + const quantity = + (await this.userWorkspaceService.getWorkspaceMemberCount( + user.defaultWorkspaceId, + )) || 1; const stripeCustomerId = ( await this.billingSubscriptionRepository.findOneBy({ @@ -204,16 +207,6 @@ export class BillingService { }) )?.stripeCustomerId; - try { - quantity = await this.userWorkspaceService.getWorkspaceMemberCount( - user.defaultWorkspaceId, - ); - } catch (e) { - this.logger.error( - `Failed to get workspace member count for workspace ${user.defaultWorkspaceId}`, - ); - } - const session = await this.stripeService.createCheckoutSession( user, priceId, @@ -260,6 +253,26 @@ export class BillingService { | Stripe.CustomerSubscriptionCreatedEvent.Data | Stripe.CustomerSubscriptionDeletedEvent.Data, ) { + const workspace = this.workspaceRepository.find({ + where: { id: workspaceId }, + }); + + if (!workspace) { + return; + } + + await this.workspaceRepository.update(workspaceId, { + subscriptionStatus: data.object.status, + }); + + const billingSubscription = await this.getCurrentBillingSubscription({ + workspaceId, + }); + + if (!billingSubscription) { + return; + } + await this.billingSubscriptionRepository.upsert( { workspaceId: workspaceId, @@ -274,18 +287,6 @@ export class BillingService { }, ); - await this.workspaceRepository.update(workspaceId, { - subscriptionStatus: data.object.status, - }); - - const billingSubscription = await this.getCurrentBillingSubscription({ - workspaceId, - }); - - if (!billingSubscription) { - return; - } - await this.billingSubscriptionItemRepository.upsert( data.object.items.data.map((item) => { return { diff --git a/packages/twenty-server/src/engine/core-modules/billing/jobs/update-subscription.job.ts b/packages/twenty-server/src/engine/core-modules/billing/jobs/update-subscription.job.ts index 8dc74ab6c66f..cf3ff0367a8d 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/jobs/update-subscription.job.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/jobs/update-subscription.job.ts @@ -21,7 +21,7 @@ export class UpdateSubscriptionJob const workspaceMembersCount = await this.userWorkspaceService.getWorkspaceMemberCount(data.workspaceId); - if (workspaceMembersCount <= 0) { + if (!workspaceMembersCount || workspaceMembersCount <= 0) { return; } diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts index e61d1337f176..0e8fc4c6f738 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts @@ -70,17 +70,23 @@ export class UserWorkspaceService extends TypeOrmQueryService { this.eventEmitter.emit('workspaceMember.created', payload); } - public async getWorkspaceMemberCount(workspaceId: string): Promise { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); + public async getWorkspaceMemberCount( + workspaceId: string, + ): Promise { + try { + const dataSourceSchema = + this.workspaceDataSourceService.getSchemaName(workspaceId); - return ( - await this.workspaceDataSourceService.executeRawQuery( - `SELECT * FROM ${dataSourceSchema}."workspaceMember"`, - [], - workspaceId, - ) - ).length; + return ( + await this.workspaceDataSourceService.executeRawQuery( + `SELECT * FROM ${dataSourceSchema}."workspaceMember"`, + [], + workspaceId, + ) + ).length; + } catch { + return undefined; + } } async checkUserWorkspaceExists( diff --git a/packages/twenty-server/src/engine/core-modules/user/services/user.service.spec.ts b/packages/twenty-server/src/engine/core-modules/user/services/user.service.spec.ts index 64228c0eaf20..d7bef57c1bbb 100644 --- a/packages/twenty-server/src/engine/core-modules/user/services/user.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/user/services/user.service.spec.ts @@ -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(); diff --git a/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts b/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts index 998066e0b6ff..f3128670bbe5 100644 --- a/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts @@ -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 { constructor( @InjectRepository(User, 'core') private readonly userRepository: Repository, - @InjectRepository(UserWorkspace, 'core') - private readonly userWorkspaceRepository: Repository, 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 { 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); - await workspaceDataSource?.query( - `DELETE FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "userId" = '${userId}'`, + const workspaceMembers = await workspaceDataSource?.query( + `SELECT * FROM ${dataSourceMetadata.schema}."workspaceMember"`, ); + const workspaceMember = workspaceMembers.filter( + (member: ObjectRecord) => + member.userId === userId, + )?.[0]; - await this.userWorkspaceRepository.delete({ userId }); - - await this.userRepository.delete(user.id); - - 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 }, - }); + assert(workspaceMember, 'WorkspaceMember not found'); - if (userWorkspaces.length === 0) { - await this.userRepository.delete({ id: userId }); + if (workspaceMembers.length === 1) { + await this.workspaceService.deleteWorkspace(workspaceId); - return; + return user; } - const user = await this.userRepository.findOne({ - where: { - id: userId, - }, - }); + await workspaceDataSource?.query( + `DELETE FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "userId" = '${userId}'`, + ); + const payload = + new ObjectRecordDeleteEvent(); - if (!user) { - throw new Error(`User ${userId} not found in workspace ${workspaceId}`); - } + payload.workspaceId = workspaceId; + payload.properties = { + before: workspaceMember, + }; + payload.recordId = workspaceMember.id; - if (user.defaultWorkspaceId === workspaceId) { - await this.userRepository.update( - { id: userId }, - { - defaultWorkspaceId: userWorkspaces[0].workspaceId, - }, - ); - } + this.eventEmitter.emit('workspaceMember.deleted', payload); + + return user; } } diff --git a/packages/twenty-server/src/engine/core-modules/user/user.module.ts b/packages/twenty-server/src/engine/core-modules/user/user.module.ts index 81b3a708347c..37b92b46bcb0 100644 --- a/packages/twenty-server/src/engine/core-modules/user/user.module.ts +++ b/packages/twenty-server/src/engine/core-modules/user/user.module.ts @@ -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], diff --git a/packages/twenty-server/src/engine/core-modules/workspace/handle-workspace-member-deleted.job.ts b/packages/twenty-server/src/engine/core-modules/workspace/handle-workspace-member-deleted.job.ts index 7f36247c1eb1..8ec5dad7ded7 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/handle-workspace-member-deleted.job.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/handle-workspace-member-deleted.job.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { MessageQueueJob } from 'src/engine/integrations/message-queue/interfaces/message-queue-job.interface'; -import { UserService } from 'src/engine/core-modules/user/services/user.service'; +import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service'; export type HandleWorkspaceMemberDeletedJobData = { workspaceId: string; @@ -12,11 +12,14 @@ export type HandleWorkspaceMemberDeletedJobData = { export class HandleWorkspaceMemberDeletedJob implements MessageQueueJob { - constructor(private readonly userService: UserService) {} + constructor(private readonly workspaceService: WorkspaceService) {} async handle(data: HandleWorkspaceMemberDeletedJobData): Promise { const { workspaceId, userId } = data; - await this.userService.handleRemoveWorkspaceMember(workspaceId, userId); + await this.workspaceService.handleRemoveWorkspaceMember( + workspaceId, + userId, + ); } } diff --git a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts index 532ae2fa6eaf..f4248d467743 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts @@ -13,18 +13,18 @@ import { ActivateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/a import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { BillingService } from 'src/engine/core-modules/billing/billing.service'; -import { UserService } from 'src/engine/core-modules/user/services/user.service'; export class WorkspaceService extends TypeOrmQueryService { constructor( @InjectRepository(Workspace, 'core') private readonly workspaceRepository: Repository, + @InjectRepository(User, 'core') + private readonly userRepository: Repository, @InjectRepository(UserWorkspace, 'core') private readonly userWorkspaceRepository: Repository, private readonly workspaceManagerService: WorkspaceManagerService, private readonly userWorkspaceService: UserWorkspaceService, private readonly billingService: BillingService, - private readonly userService: UserService, ) { super(workspaceRepository); } @@ -49,7 +49,7 @@ export class WorkspaceService extends TypeOrmQueryService { return await this.workspaceManagerService.doesDataSourceExist(id); } - async solfDeleteWorkspace(id: string) { + async softDeleteWorkspace(id: string) { const workspace = await this.workspaceRepository.findOneBy({ id }); assert(workspace, 'Workspace not found'); @@ -67,14 +67,12 @@ export class WorkspaceService extends TypeOrmQueryService { workspaceId: id, }); - const workspace = await this.solfDeleteWorkspace(id); + const workspace = await this.softDeleteWorkspace(id); for (const userWorkspace of userWorkspaces) { - await this.userService.handleRemoveWorkspaceMember( - id, - userWorkspace.userId, - ); + await this.handleRemoveWorkspaceMember(id, userWorkspace.userId); } + await this.workspaceRepository.delete(id); return workspace; @@ -85,4 +83,46 @@ export class WorkspaceService extends TypeOrmQueryService { .find() .then((workspaces) => workspaces.map((workspace) => workspace.id)); } + + 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, + }, + ); + } + } } diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts index 63b821476948..ad5ad20ed2fe 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts @@ -12,9 +12,9 @@ import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; -import { UserModule } from 'src/engine/core-modules/user/user.module'; import { WorkspaceWorkspaceMemberListener } from 'src/engine/core-modules/workspace/workspace-workspace-member.listener'; import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module'; +import { User } from 'src/engine/core-modules/user/user.entity'; import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts'; import { Workspace } from './workspace.entity'; @@ -30,14 +30,13 @@ import { WorkspaceService } from './services/workspace.service'; FileUploadModule, WorkspaceCacheVersionModule, NestjsQueryTypeOrmModule.forFeature( - [Workspace, UserWorkspace, FeatureFlagEntity], + [User, Workspace, UserWorkspace, FeatureFlagEntity], 'core', ), UserWorkspaceModule, WorkspaceManagerModule, DataSourceModule, TypeORMModule, - UserModule, ], services: [WorkspaceService], resolvers: workspaceAutoResolverOpts, diff --git a/packages/twenty-server/src/engine/integrations/message-queue/jobs.module.ts b/packages/twenty-server/src/engine/integrations/message-queue/jobs.module.ts index 2274586ecafb..5ef30f9be140 100644 --- a/packages/twenty-server/src/engine/integrations/message-queue/jobs.module.ts +++ b/packages/twenty-server/src/engine/integrations/message-queue/jobs.module.ts @@ -16,6 +16,12 @@ import { EmailModule } from 'src/engine/integrations/email/email.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; import { CleanInactiveWorkspaceJob } from 'src/engine/workspace-manager/workspace-cleaner/crons/clean-inactive-workspace.job'; +import { CalendarEventParticipantModule } from 'src/modules/calendar/services/calendar-event-participant/calendar-event-participant.module'; +import { GmailFetchMessageContentFromCacheModule } from 'src/modules/messaging/services/gmail-fetch-message-content-from-cache/gmail-fetch-message-content-from-cache.module'; +import { GmailFullSyncModule } from 'src/modules/messaging/services/gmail-full-sync/gmail-full-sync.module'; +import { GmailPartialSyncModule } from 'src/modules/messaging/services/gmail-partial-sync/gmail-partial-sync.module'; +import { TimelineActivityModule } from 'src/modules/timeline/timeline-activity.module'; +import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; import { CalendarMessagingParticipantJobModule } from 'src/modules/calendar-messaging-participant/jobs/calendar-messaging-participant-job.module'; import { CalendarCronJobModule } from 'src/modules/calendar/crons/jobs/calendar-cron-job.module'; import { CalendarJobModule } from 'src/modules/calendar/jobs/calendar-job.module'; @@ -34,6 +40,12 @@ import { TimelineJobModule } from 'src/modules/timeline/jobs/timeline-job.module DataSeedDemoWorkspaceModule, BillingModule, UserWorkspaceModule, + WorkspaceModule, + GmailFullSyncModule, + GmailFetchMessageContentFromCacheModule, + GmailPartialSyncModule, + CalendarEventParticipantModule, + TimelineActivityModule, StripeModule, // JobsModules WorkspaceQueryRunnerJobModule, 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-incomplete-workspaces.command.ts index 372968b1c8a5..30c980c4e0c1 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-incomplete-workspaces.command.ts @@ -83,7 +83,7 @@ export class DeleteIncompleteWorkspacesCommand extends CommandRunner { } name: '${incompleteWorkspace.displayName}'`, ); if (!options.dryRun) { - await this.workspaceService.solfDeleteWorkspace(incompleteWorkspace.id); + await this.workspaceService.softDeleteWorkspace(incompleteWorkspace.id); } } }