diff --git a/packages/twenty-front/src/modules/ui/input/button/components/MainButton.tsx b/packages/twenty-front/src/modules/ui/input/button/components/MainButton.tsx index ab23ceecc..333eaeb2f 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/MainButton.tsx +++ b/packages/twenty-front/src/modules/ui/input/button/components/MainButton.tsx @@ -1,6 +1,6 @@ -import React from 'react'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import React from 'react'; import { IconComponent } from 'twenty-ui'; export type MainButtonVariant = 'primary' | 'secondary'; @@ -79,7 +79,7 @@ const StyledButton = styled.button< padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(3)}; width: ${({ fullWidth, width }) => fullWidth ? '100%' : width ? `${width}px` : 'auto'}; - ${({ theme, variant }) => { + ${({ theme, variant, disabled }) => { switch (variant) { case 'secondary': return ` @@ -90,7 +90,11 @@ const StyledButton = styled.button< default: return ` &:hover { - background: ${theme.background.primaryInvertedHover}}; + background: ${ + !disabled + ? theme.background.primaryInvertedHover + : theme.background.secondary + };}; } `; } diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-set-workspace-activation-status.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-set-workspace-activation-status.command.ts index 57700e9b7..d9dd84e33 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-set-workspace-activation-status.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-set-workspace-activation-status.command.ts @@ -6,7 +6,7 @@ import { Command, CommandRunner, Option } from 'nest-commander'; import { Repository } from 'typeorm'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; -import { BillingService } from 'src/engine/core-modules/billing/billing.service'; +import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { Workspace, WorkspaceActivationStatus, @@ -32,7 +32,7 @@ export class SetWorkspaceActivationStatusCommand extends CommandRunner { private readonly typeORMService: TypeORMService, private readonly dataSourceService: DataSourceService, private readonly workspaceCacheVersionService: WorkspaceCacheVersionService, - private readonly billingService: BillingService, + private readonly billingSubscriptionService: BillingSubscriptionService, ) { super(); } @@ -56,7 +56,7 @@ export class SetWorkspaceActivationStatusCommand extends CommandRunner { activeSubscriptionWorkspaceIds = [options.workspaceId]; } else { activeSubscriptionWorkspaceIds = - await this.billingService.getActiveSubscriptionWorkspaceIds(); + await this.billingSubscriptionService.getActiveSubscriptionWorkspaceIds(); } if (!activeSubscriptionWorkspaceIds.length) { diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts index a734c2694..2a090c79e 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts @@ -32,6 +32,7 @@ import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/stan import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; +import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module'; import { AuthResolver } from './auth.resolver'; diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts index 0715d3122..3527d6652 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts @@ -9,14 +9,14 @@ import { import { Response } from 'express'; +import { GoogleAPIsOauthExchangeCodeForTokenGuard } from 'src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard'; import { GoogleAPIsOauthRequestCodeGuard } from 'src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard'; import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service'; import { TokenService } from 'src/engine/core-modules/auth/services/token.service'; -import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; -import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; -import { LoadServiceWithWorkspaceContext } from 'src/engine/twenty-orm/context/load-service-with-workspace.context'; -import { GoogleAPIsOauthExchangeCodeForTokenGuard } from 'src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard'; import { GoogleAPIsRequest } from 'src/engine/core-modules/auth/types/google-api-request.type'; +import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; +import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; +import { LoadServiceWithWorkspaceContext } from 'src/engine/twenty-orm/context/load-service-with-workspace.context'; @Controller('auth/google-apis') export class GoogleAPIsAuthController { @@ -93,10 +93,11 @@ export class GoogleAPIsAuthController { workspaceId, ); - await onboardingServiceInstance.skipSyncEmailOnboardingStep( + await onboardingServiceInstance.toggleOnboardingConnectAccountCompletion({ userId, workspaceId, - ); + value: true, + }); } return res.redirect( diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.spec.ts index b6b381ec0..7313ebf0f 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.spec.ts @@ -9,6 +9,7 @@ import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-u import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service'; +import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; describe('SignInUpService', () => { let service: SignInUpService; @@ -21,10 +22,6 @@ describe('SignInUpService', () => { provide: FileUploadService, useValue: {}, }, - { - provide: UserWorkspaceService, - useValue: {}, - }, { provide: getRepositoryToken(Workspace, 'core'), useValue: {}, @@ -34,7 +31,11 @@ describe('SignInUpService', () => { useValue: {}, }, { - provide: EnvironmentService, + provide: UserWorkspaceService, + useValue: {}, + }, + { + provide: OnboardingService, useValue: {}, }, { @@ -42,7 +43,7 @@ describe('SignInUpService', () => { useValue: {}, }, { - provide: WorkspaceService, + provide: EnvironmentService, useValue: {}, }, ], diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts index b53284dd2..0cc3f2f03 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts @@ -18,9 +18,9 @@ import { hashPassword, } from 'src/engine/core-modules/auth/auth.util'; import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service'; +import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { User } from 'src/engine/core-modules/user/user.entity'; -import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service'; import { Workspace, WorkspaceActivationStatus, @@ -40,6 +40,7 @@ export type SignInUpServiceInput = { }; @Injectable() +// eslint-disable-next-line @nx/workspace-inject-workspace-repository export class SignInUpService { constructor( private readonly fileUploadService: FileUploadService, @@ -48,7 +49,7 @@ export class SignInUpService { @InjectRepository(User, 'core') private readonly userRepository: Repository, private readonly userWorkspaceService: UserWorkspaceService, - private readonly workspaceService: WorkspaceService, + private readonly onboardingService: OnboardingService, private readonly httpService: HttpService, private readonly environmentService: EnvironmentService, ) {} @@ -221,6 +222,14 @@ export class SignInUpService { await this.userWorkspaceService.create(user.id, workspace.id); + if (user.firstName !== '' || user.lastName === '') { + await this.onboardingService.toggleOnboardingCreateProfileCompletion({ + userId: user.id, + workspaceId: workspace.id, + value: true, + }); + } + return user; } diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts index 4e71805b9..d38dfd1d5 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts @@ -1,19 +1,18 @@ import { Controller, Headers, - Req, - RawBodyRequest, Logger, Post, + RawBodyRequest, + Req, Res, } from '@nestjs/common'; import { Response } from 'express'; -import { - BillingWorkspaceService, - WebhookEvent, -} from 'src/engine/core-modules/billing/billing.workspace-service'; +import { WebhookEvent } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service'; +import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; +import { BillingWebhookService } from 'src/engine/core-modules/billing/services/billing-webhook.service'; import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service'; @Controller('billing') @@ -22,7 +21,8 @@ export class BillingController { constructor( private readonly stripeService: StripeService, - private readonly billingWorkspaceService: BillingWorkspaceService, + private readonly billingSubscriptionService: BillingSubscriptionService, + private readonly billingWehbookService: BillingWebhookService, ) {} @Post('/webhooks') @@ -42,7 +42,7 @@ export class BillingController { ); if (event.type === WebhookEvent.SETUP_INTENT_SUCCEEDED) { - await this.billingWorkspaceService.handleUnpaidInvoices(event.data); + await this.billingSubscriptionService.handleUnpaidInvoices(event.data); } if ( @@ -58,7 +58,7 @@ export class BillingController { return; } - await this.billingWorkspaceService.upsertBillingSubscription( + await this.billingWehbookService.processStripeEvent( workspaceId, event.data, ); diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts index a4462607a..cb66d9109 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts @@ -2,16 +2,17 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { BillingController } from 'src/engine/core-modules/billing/billing.controller'; -import { BillingService } from 'src/engine/core-modules/billing/billing.service'; -import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module'; -import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; -import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { BillingResolver } from 'src/engine/core-modules/billing/billing.resolver'; +import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity'; +import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { BillingWorkspaceMemberListener } from 'src/engine/core-modules/billing/listeners/billing-workspace-member.listener'; -import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module'; +import { BillingPortalWorkspaceService } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service'; +import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; +import { BillingWebhookService } from 'src/engine/core-modules/billing/services/billing-webhook.service'; +import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; -import { BillingWorkspaceService } from 'src/engine/core-modules/billing/billing.workspace-service'; +import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @Module({ imports: [ @@ -29,11 +30,16 @@ import { BillingWorkspaceService } from 'src/engine/core-modules/billing/billing ], controllers: [BillingController], providers: [ - BillingService, - BillingWorkspaceService, + BillingSubscriptionService, + BillingWebhookService, + BillingPortalWorkspaceService, BillingResolver, BillingWorkspaceMemberListener, ], - exports: [BillingService, BillingWorkspaceService], + exports: [ + BillingSubscriptionService, + BillingPortalWorkspaceService, + BillingWebhookService, + ], }) export class BillingModule {} diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts index 0f4d772aa..a1bf997b8 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts @@ -1,43 +1,34 @@ import { UseGuards } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; -import { - AvailableProduct, - BillingWorkspaceService, -} from 'src/engine/core-modules/billing/billing.workspace-service'; +import { AvailableProduct } from 'src/engine/core-modules/billing/interfaces/available-product.interface'; + import { BillingSessionInput } from 'src/engine/core-modules/billing/dto/billing-session.input'; import { CheckoutSessionInput } from 'src/engine/core-modules/billing/dto/checkout-session.input'; import { ProductPricesEntity } from 'src/engine/core-modules/billing/dto/product-prices.entity'; import { ProductInput } from 'src/engine/core-modules/billing/dto/product.input'; import { SessionEntity } from 'src/engine/core-modules/billing/dto/session.entity'; import { UpdateBillingEntity } from 'src/engine/core-modules/billing/dto/update-billing.entity'; +import { BillingPortalWorkspaceService } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service'; +import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; +import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service'; import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; -import { assert } from 'src/utils/assert'; @Resolver() export class BillingResolver { constructor( - private readonly billingWorkspaceService: BillingWorkspaceService, + private readonly billingSubscriptionService: BillingSubscriptionService, + private readonly billingPortalWorkspaceService: BillingPortalWorkspaceService, + private readonly stripeService: StripeService, ) {} @Query(() => ProductPricesEntity) async getProductPrices(@Args() { product }: ProductInput) { - const stripeProductId = - this.billingWorkspaceService.getProductStripeId(product); - - assert( - stripeProductId, - `Product '${product}' not found, available products are ['${Object.values( - AvailableProduct, - ).join("','")}']`, - ); - - const productPrices = - await this.billingWorkspaceService.getProductPrices(stripeProductId); + const productPrices = await this.stripeService.getStripePrices(product); return { totalNumberOfPrices: productPrices.length, @@ -52,7 +43,7 @@ export class BillingResolver { @Args() { returnUrlPath }: BillingSessionInput, ) { return { - url: await this.billingWorkspaceService.computeBillingPortalSessionURL( + url: await this.billingPortalWorkspaceService.computeBillingPortalSessionURL( user.defaultWorkspaceId, returnUrlPath, ), @@ -66,32 +57,22 @@ export class BillingResolver { @AuthUser() user: User, @Args() { recurringInterval, successUrlPath }: CheckoutSessionInput, ) { - const stripeProductId = this.billingWorkspaceService.getProductStripeId( + const productPrice = await this.stripeService.getStripePrice( AvailableProduct.BasePlan, + recurringInterval, ); - assert( - stripeProductId, - 'BasePlan productId not found, please check your BILLING_STRIPE_BASE_PLAN_PRODUCT_ID env variable', - ); - - const productPrices = - await this.billingWorkspaceService.getProductPrices(stripeProductId); - - const stripePriceId = productPrices.filter( - (price) => price.recurringInterval === recurringInterval, - )?.[0]?.stripePriceId; - - assert( - stripePriceId, - `BasePlan priceId not found, please check body.recurringInterval and product '${AvailableProduct.BasePlan}' prices`, - ); + if (!productPrice) { + throw new Error( + 'Product price not found for the given recurring interval', + ); + } return { - url: await this.billingWorkspaceService.computeCheckoutSessionURL( + url: await this.billingPortalWorkspaceService.computeCheckoutSessionURL( user, workspace, - stripePriceId, + productPrice.stripePriceId, successUrlPath, ), }; @@ -100,7 +81,7 @@ export class BillingResolver { @Mutation(() => UpdateBillingEntity) @UseGuards(JwtAuthGuard) async updateBillingSubscription(@AuthUser() user: User) { - await this.billingWorkspaceService.updateBillingSubscription(user); + await this.billingSubscriptionService.applyBillingSubscription(user); return { success: true }; } 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 deleted file mode 100644 index 1994f7705..000000000 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.service.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { In, Repository } from 'typeorm'; - -import { - BillingSubscription, - SubscriptionStatus, -} from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; - -@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() { - 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: FeatureFlagKey.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/billing/billing.workspace-service.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.workspace-service.ts deleted file mode 100644 index be0ecb355..000000000 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.workspace-service.ts +++ /dev/null @@ -1,354 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import Stripe from 'stripe'; -import { Not, Repository } from 'typeorm'; - -import { BillingService } from 'src/engine/core-modules/billing/billing.service'; -import { ProductPriceEntity } from 'src/engine/core-modules/billing/dto/product-price.entity'; -import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity'; -import { - BillingSubscription, - SubscriptionInterval, - SubscriptionStatus, -} from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; -import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service'; -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; -import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; -import { User } from 'src/engine/core-modules/user/user.entity'; -import { - Workspace, - WorkspaceActivationStatus, -} from 'src/engine/core-modules/workspace/workspace.entity'; -import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; -import { assert } from 'src/utils/assert'; - -export enum AvailableProduct { - BasePlan = 'base-plan', -} - -export enum WebhookEvent { - CUSTOMER_SUBSCRIPTION_CREATED = 'customer.subscription.created', - CUSTOMER_SUBSCRIPTION_UPDATED = 'customer.subscription.updated', - CUSTOMER_SUBSCRIPTION_DELETED = 'customer.subscription.deleted', - SETUP_INTENT_SUCCEEDED = 'setup_intent.succeeded', -} - -@Injectable() -export class BillingWorkspaceService { - protected readonly logger = new Logger(BillingService.name); - constructor( - private readonly stripeService: StripeService, - private readonly userWorkspaceService: UserWorkspaceService, - private readonly environmentService: EnvironmentService, - @InjectRepository(BillingSubscription, 'core') - private readonly billingSubscriptionRepository: Repository, - @InjectRepository(FeatureFlagEntity, 'core') - private readonly featureFlagRepository: Repository, - @InjectRepository(BillingSubscriptionItem, 'core') - private readonly billingSubscriptionItemRepository: Repository, - @InjectRepository(Workspace, 'core') - private readonly workspaceRepository: Repository, - ) {} - - async isBillingEnabledForWorkspace(workspaceId: string) { - const isFreeAccessEnabled = await this.featureFlagRepository.findOneBy({ - workspaceId, - key: FeatureFlagKey.IsFreeAccessEnabled, - value: true, - }); - - return ( - !isFreeAccessEnabled && this.environmentService.get('IS_BILLING_ENABLED') - ); - } - - getProductStripeId(product: AvailableProduct) { - if (product === AvailableProduct.BasePlan) { - return this.environmentService.get('BILLING_STRIPE_BASE_PLAN_PRODUCT_ID'); - } - } - - async getProductPrices(stripeProductId: string) { - const productPrices = - await this.stripeService.getProductPrices(stripeProductId); - - return this.formatProductPrices(productPrices.data); - } - - formatProductPrices(prices: Stripe.Price[]) { - const result: Record = {}; - - prices.forEach((item) => { - const interval = item.recurring?.interval; - - if (!interval || !item.unit_amount) { - return; - } - if ( - !result[interval] || - item.created > (result[interval]?.created || 0) - ) { - result[interval] = { - unitAmount: item.unit_amount, - recurringInterval: interval, - created: item.created, - stripePriceId: item.id, - }; - } - }); - - return Object.values(result).sort((a, b) => a.unitAmount - b.unitAmount); - } - - async getCurrentBillingSubscription(criteria: { - workspaceId?: string; - stripeCustomerId?: string; - }) { - const notCanceledSubscriptions = - await this.billingSubscriptionRepository.find({ - where: { ...criteria, status: Not(SubscriptionStatus.Canceled) }, - relations: ['billingSubscriptionItems'], - }); - - assert( - notCanceledSubscriptions.length <= 1, - `More than one not canceled subscription for workspace ${criteria.workspaceId}`, - ); - - return notCanceledSubscriptions?.[0]; - } - - async getBillingSubscription(stripeSubscriptionId: string) { - return this.billingSubscriptionRepository.findOneOrFail({ - where: { stripeSubscriptionId }, - }); - } - - async getStripeCustomerId(workspaceId: string) { - const subscriptions = await this.billingSubscriptionRepository.find({ - where: { workspaceId }, - }); - - return subscriptions?.[0]?.stripeCustomerId; - } - - async getBillingSubscriptionItem( - workspaceId: string, - stripeProductId = this.environmentService.get( - 'BILLING_STRIPE_BASE_PLAN_PRODUCT_ID', - ), - ) { - const billingSubscription = await this.getCurrentBillingSubscription({ - workspaceId, - }); - - if (!billingSubscription) { - throw new Error( - `Cannot find billingSubscriptionItem for product ${stripeProductId} for workspace ${workspaceId}`, - ); - } - - const billingSubscriptionItem = - billingSubscription.billingSubscriptionItems.filter( - (billingSubscriptionItem) => - billingSubscriptionItem.stripeProductId === stripeProductId, - )?.[0]; - - if (!billingSubscriptionItem) { - throw new Error( - `Cannot find billingSubscriptionItem for product ${stripeProductId} for workspace ${workspaceId}`, - ); - } - - return billingSubscriptionItem; - } - - async computeBillingPortalSessionURL( - workspaceId: string, - returnUrlPath?: string, - ) { - const stripeCustomerId = await this.getStripeCustomerId(workspaceId); - - if (!stripeCustomerId) { - return; - } - - const frontBaseUrl = this.environmentService.get('FRONT_BASE_URL'); - const returnUrl = returnUrlPath - ? frontBaseUrl + returnUrlPath - : frontBaseUrl; - - const session = await this.stripeService.createBillingPortalSession( - stripeCustomerId, - returnUrl, - ); - - assert(session.url, 'Error: missing billingPortal.session.url'); - - return session.url; - } - - async updateBillingSubscription(user: User) { - const billingSubscription = await this.getCurrentBillingSubscription({ - workspaceId: user.defaultWorkspaceId, - }); - const newInterval = - billingSubscription?.interval === SubscriptionInterval.Year - ? SubscriptionInterval.Month - : SubscriptionInterval.Year; - const billingSubscriptionItem = await this.getBillingSubscriptionItem( - user.defaultWorkspaceId, - ); - const stripeProductId = this.getProductStripeId(AvailableProduct.BasePlan); - - if (!stripeProductId) { - throw new Error('Stripe product id not found for basePlan'); - } - const productPrices = await this.getProductPrices(stripeProductId); - - const stripePriceId = productPrices.filter( - (price) => price.recurringInterval === newInterval, - )?.[0]?.stripePriceId; - - await this.stripeService.updateBillingSubscriptionItem( - billingSubscriptionItem, - stripePriceId, - ); - } - - async computeCheckoutSessionURL( - user: User, - workspace: Workspace, - priceId: string, - successUrlPath?: string, - ): Promise { - const frontBaseUrl = this.environmentService.get('FRONT_BASE_URL'); - const successUrl = successUrlPath - ? frontBaseUrl + successUrlPath - : frontBaseUrl; - - const quantity = - (await this.userWorkspaceService.getUserCount(workspace.id)) || 1; - - const stripeCustomerId = ( - await this.billingSubscriptionRepository.findOneBy({ - workspaceId: user.defaultWorkspaceId, - }) - )?.stripeCustomerId; - - const session = await this.stripeService.createCheckoutSession( - user, - priceId, - quantity, - successUrl, - frontBaseUrl, - stripeCustomerId, - ); - - assert(session.url, 'Error: missing checkout.session.url'); - - return session.url; - } - - async deleteSubscription(workspaceId: string) { - const subscriptionToCancel = await this.getCurrentBillingSubscription({ - workspaceId, - }); - - if (subscriptionToCancel) { - await this.stripeService.cancelSubscription( - subscriptionToCancel.stripeSubscriptionId, - ); - await this.billingSubscriptionRepository.delete(subscriptionToCancel.id); - } - } - - async handleUnpaidInvoices(data: Stripe.SetupIntentSucceededEvent.Data) { - const billingSubscription = await this.getCurrentBillingSubscription({ - stripeCustomerId: data.object.customer as string, - }); - - if (billingSubscription?.status === 'unpaid') { - await this.stripeService.collectLastInvoice( - billingSubscription.stripeSubscriptionId, - ); - } - } - - async upsertBillingSubscription( - workspaceId: string, - data: - | Stripe.CustomerSubscriptionUpdatedEvent.Data - | Stripe.CustomerSubscriptionCreatedEvent.Data - | Stripe.CustomerSubscriptionDeletedEvent.Data, - ) { - const workspace = await this.workspaceRepository.findOne({ - where: { id: workspaceId }, - }); - - if (!workspace) { - return; - } - - await this.billingSubscriptionRepository.upsert( - { - workspaceId: workspaceId, - stripeCustomerId: data.object.customer as string, - stripeSubscriptionId: data.object.id, - status: data.object.status, - interval: data.object.items.data[0].plan.interval, - }, - { - conflictPaths: ['stripeSubscriptionId'], - skipUpdateIfNoValuesChanged: true, - }, - ); - - const billingSubscription = await this.getBillingSubscription( - data.object.id, - ); - - await this.billingSubscriptionItemRepository.upsert( - data.object.items.data.map((item) => { - return { - billingSubscriptionId: billingSubscription.id, - stripeProductId: item.price.product as string, - stripePriceId: item.price.id, - stripeSubscriptionItemId: item.id, - quantity: item.quantity, - }; - }), - { - conflictPaths: ['billingSubscriptionId', 'stripeProductId'], - skipUpdateIfNoValuesChanged: true, - }, - ); - - await this.featureFlagRepository.delete({ - workspaceId, - key: FeatureFlagKey.IsFreeAccessEnabled, - }); - - if ( - data.object.status === SubscriptionStatus.Canceled || - data.object.status === SubscriptionStatus.Unpaid - ) { - await this.workspaceRepository.update(workspaceId, { - activationStatus: WorkspaceActivationStatus.INACTIVE, - }); - } - - if ( - (data.object.status === SubscriptionStatus.Active || - data.object.status === SubscriptionStatus.Trialing || - data.object.status === SubscriptionStatus.PastDue) && - workspace.activationStatus == WorkspaceActivationStatus.INACTIVE - ) { - await this.workspaceRepository.update(workspaceId, { - activationStatus: WorkspaceActivationStatus.ACTIVE, - }); - } - } -} diff --git a/packages/twenty-server/src/engine/core-modules/billing/dto/product.input.ts b/packages/twenty-server/src/engine/core-modules/billing/dto/product.input.ts index 1bab951e2..f58f681e0 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/dto/product.input.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/dto/product.input.ts @@ -2,7 +2,7 @@ import { ArgsType, Field } from '@nestjs/graphql'; import { IsNotEmpty, IsString } from 'class-validator'; -import { AvailableProduct } from 'src/engine/core-modules/billing/billing.workspace-service'; +import { AvailableProduct } from 'src/engine/core-modules/billing/interfaces/available-product.interface'; @ArgsType() export class ProductInput { diff --git a/packages/twenty-server/src/engine/core-modules/billing/interfaces/available-product.interface.ts b/packages/twenty-server/src/engine/core-modules/billing/interfaces/available-product.interface.ts new file mode 100644 index 000000000..489584813 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/interfaces/available-product.interface.ts @@ -0,0 +1,3 @@ +export enum AvailableProduct { + BasePlan = 'base-plan', +} 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 02e803b54..14929639f 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 @@ -1,6 +1,6 @@ import { Logger, Scope } from '@nestjs/common'; -import { BillingWorkspaceService } from 'src/engine/core-modules/billing/billing.workspace-service'; +import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; @@ -16,7 +16,7 @@ export class UpdateSubscriptionJob { protected readonly logger = new Logger(UpdateSubscriptionJob.name); constructor( - private readonly billingWorkspaceService: BillingWorkspaceService, + private readonly billingSubscriptionService: BillingSubscriptionService, private readonly userWorkspaceService: UserWorkspaceService, private readonly stripeService: StripeService, ) {} @@ -33,7 +33,7 @@ export class UpdateSubscriptionJob { try { const billingSubscriptionItem = - await this.billingWorkspaceService.getBillingSubscriptionItem( + await this.billingSubscriptionService.getCurrentBillingSubscriptionItem( data.workspaceId, ); diff --git a/packages/twenty-server/src/engine/core-modules/billing/listeners/billing-workspace-member.listener.ts b/packages/twenty-server/src/engine/core-modules/billing/listeners/billing-workspace-member.listener.ts index 4fadaaf5e..d318b7277 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/listeners/billing-workspace-member.listener.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/listeners/billing-workspace-member.listener.ts @@ -1,23 +1,23 @@ import { Injectable } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; -import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; -import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; -import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event'; -import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; import { UpdateSubscriptionJob, UpdateSubscriptionJobData, } from 'src/engine/core-modules/billing/jobs/update-subscription.job'; +import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; +import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event'; import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator'; -import { BillingWorkspaceService } from 'src/engine/core-modules/billing/billing.workspace-service'; +import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; +import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; +import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @Injectable() export class BillingWorkspaceMemberListener { constructor( @InjectMessageQueue(MessageQueue.billingQueue) private readonly messageQueueService: MessageQueueService, - private readonly billingWorkspaceService: BillingWorkspaceService, + private readonly environmentService: EnvironmentService, ) {} @OnEvent('workspaceMember.created') @@ -25,12 +25,7 @@ export class BillingWorkspaceMemberListener { async handleCreateOrDeleteEvent( payload: ObjectRecordCreateEvent, ) { - const isBillingEnabledForWorkspace = - await this.billingWorkspaceService.isBillingEnabledForWorkspace( - payload.workspaceId, - ); - - if (!isBillingEnabledForWorkspace) { + if (!this.environmentService.get('IS_BILLING_ENABLED')) { return; } diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts new file mode 100644 index 000000000..506a97dd7 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts @@ -0,0 +1,93 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; +import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; +import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service'; +import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; +import { User } from 'src/engine/core-modules/user/user.entity'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; +import { assert } from 'src/utils/assert'; + +export enum WebhookEvent { + CUSTOMER_SUBSCRIPTION_CREATED = 'customer.subscription.created', + CUSTOMER_SUBSCRIPTION_UPDATED = 'customer.subscription.updated', + CUSTOMER_SUBSCRIPTION_DELETED = 'customer.subscription.deleted', + SETUP_INTENT_SUCCEEDED = 'setup_intent.succeeded', +} + +@Injectable() +export class BillingPortalWorkspaceService { + protected readonly logger = new Logger(BillingPortalWorkspaceService.name); + constructor( + private readonly stripeService: StripeService, + private readonly userWorkspaceService: UserWorkspaceService, + private readonly environmentService: EnvironmentService, + @InjectRepository(BillingSubscription, 'core') + private readonly billingSubscriptionRepository: Repository, + private readonly billingSubscriptionService: BillingSubscriptionService, + ) {} + + async computeCheckoutSessionURL( + user: User, + workspace: Workspace, + priceId: string, + successUrlPath?: string, + ): Promise { + const frontBaseUrl = this.environmentService.get('FRONT_BASE_URL'); + const successUrl = successUrlPath + ? frontBaseUrl + successUrlPath + : frontBaseUrl; + + const quantity = + (await this.userWorkspaceService.getUserCount(workspace.id)) || 1; + + const stripeCustomerId = ( + await this.billingSubscriptionRepository.findOneBy({ + workspaceId: user.defaultWorkspaceId, + }) + )?.stripeCustomerId; + + const session = await this.stripeService.createCheckoutSession( + user, + priceId, + quantity, + successUrl, + frontBaseUrl, + stripeCustomerId, + ); + + assert(session.url, 'Error: missing checkout.session.url'); + + return session.url; + } + + async computeBillingPortalSessionURL( + workspaceId: string, + returnUrlPath?: string, + ) { + const currentSubscriptionItem = + await this.billingSubscriptionService.getCurrentBillingSubscription({ + workspaceId, + }); + + const stripeCustomerId = currentSubscriptionItem.stripeCustomerId; + + const frontBaseUrl = this.environmentService.get('FRONT_BASE_URL'); + const returnUrl = returnUrlPath + ? frontBaseUrl + returnUrlPath + : frontBaseUrl; + + const session = await this.stripeService.createBillingPortalSession( + stripeCustomerId, + returnUrl, + ); + + assert(session.url, 'Error: missing billingPortal.session.url'); + + return session.url; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts new file mode 100644 index 000000000..9b0575e87 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts @@ -0,0 +1,185 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import assert from 'assert'; + +import { User } from '@sentry/node'; +import Stripe from 'stripe'; +import { In, Not, Repository } from 'typeorm'; + +import { AvailableProduct } from 'src/engine/core-modules/billing/interfaces/available-product.interface'; + +import { + BillingSubscription, + SubscriptionInterval, + SubscriptionStatus, +} from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; +import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; + +@Injectable() +export class BillingSubscriptionService { + protected readonly logger = new Logger(BillingSubscriptionService.name); + constructor( + private readonly stripeService: StripeService, + 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, + ) {} + + /** + * @deprecated This is fully deprecated, it's only used in the migration script for 0.23 + */ + async getActiveSubscriptionWorkspaceIds() { + 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: FeatureFlagKey.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, + ]), + ); + } + + async getCurrentBillingSubscription(criteria: { + workspaceId?: string; + stripeCustomerId?: string; + }) { + const notCanceledSubscriptions = + await this.billingSubscriptionRepository.find({ + where: { ...criteria, status: Not(SubscriptionStatus.Canceled) }, + relations: ['billingSubscriptionItems'], + }); + + assert( + notCanceledSubscriptions.length <= 1, + `More than one not canceled subscription for workspace ${criteria.workspaceId}`, + ); + + return notCanceledSubscriptions?.[0]; + } + + async getCurrentBillingSubscriptionItem( + workspaceId: string, + stripeProductId = this.environmentService.get( + 'BILLING_STRIPE_BASE_PLAN_PRODUCT_ID', + ), + ) { + const billingSubscription = await this.getCurrentBillingSubscription({ + workspaceId, + }); + + if (!billingSubscription) { + throw new Error( + `Cannot find billingSubscriptionItem for product ${stripeProductId} for workspace ${workspaceId}`, + ); + } + + const billingSubscriptionItem = + billingSubscription.billingSubscriptionItems.filter( + (billingSubscriptionItem) => + billingSubscriptionItem.stripeProductId === stripeProductId, + )?.[0]; + + if (!billingSubscriptionItem) { + throw new Error( + `Cannot find billingSubscriptionItem for product ${stripeProductId} for workspace ${workspaceId}`, + ); + } + + return billingSubscriptionItem; + } + + async deleteSubscription(workspaceId: string) { + const subscriptionToCancel = await this.getCurrentBillingSubscription({ + workspaceId, + }); + + if (subscriptionToCancel) { + await this.stripeService.cancelSubscription( + subscriptionToCancel.stripeSubscriptionId, + ); + await this.billingSubscriptionRepository.delete(subscriptionToCancel.id); + } + } + + async handleUnpaidInvoices(data: Stripe.SetupIntentSucceededEvent.Data) { + const billingSubscription = await this.getCurrentBillingSubscription({ + stripeCustomerId: data.object.customer as string, + }); + + if (billingSubscription?.status === 'unpaid') { + await this.stripeService.collectLastInvoice( + billingSubscription.stripeSubscriptionId, + ); + } + } + + async applyBillingSubscription(user: User) { + const billingSubscription = await this.getCurrentBillingSubscription({ + workspaceId: user.defaultWorkspaceId, + }); + + const newInterval = + billingSubscription?.interval === SubscriptionInterval.Year + ? SubscriptionInterval.Month + : SubscriptionInterval.Year; + + const billingSubscriptionItem = + await this.getCurrentBillingSubscriptionItem(user.defaultWorkspaceId); + + const productPrice = await this.stripeService.getStripePrice( + AvailableProduct.BasePlan, + newInterval, + ); + + if (!productPrice) { + throw new Error( + `Cannot find product price for product ${AvailableProduct.BasePlan} and interval ${newInterval}`, + ); + } + + await this.stripeService.updateBillingSubscriptionItem( + billingSubscriptionItem, + productPrice.stripePriceId, + ); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook.service.ts new file mode 100644 index 000000000..347eea6de --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook.service.ts @@ -0,0 +1,99 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import Stripe from 'stripe'; +import { Repository } from 'typeorm'; + +import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity'; +import { + BillingSubscription, + SubscriptionStatus, +} from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; +import { + Workspace, + WorkspaceActivationStatus, +} from 'src/engine/core-modules/workspace/workspace.entity'; + +@Injectable() +export class BillingWebhookService { + protected readonly logger = new Logger(BillingWebhookService.name); + constructor( + @InjectRepository(BillingSubscription, 'core') + private readonly billingSubscriptionRepository: Repository, + @InjectRepository(BillingSubscriptionItem, 'core') + private readonly billingSubscriptionItemRepository: Repository, + @InjectRepository(Workspace, 'core') + private readonly workspaceRepository: Repository, + ) {} + + async processStripeEvent( + workspaceId: string, + data: + | Stripe.CustomerSubscriptionUpdatedEvent.Data + | Stripe.CustomerSubscriptionCreatedEvent.Data + | Stripe.CustomerSubscriptionDeletedEvent.Data, + ) { + const workspace = await this.workspaceRepository.findOne({ + where: { id: workspaceId }, + }); + + if (!workspace) { + return; + } + + await this.billingSubscriptionRepository.upsert( + { + workspaceId: workspaceId, + stripeCustomerId: data.object.customer as string, + stripeSubscriptionId: data.object.id, + status: data.object.status, + interval: data.object.items.data[0].plan.interval, + }, + { + conflictPaths: ['stripeSubscriptionId'], + skipUpdateIfNoValuesChanged: true, + }, + ); + + const billingSubscription = + await this.billingSubscriptionRepository.findOneOrFail({ + where: { stripeSubscriptionId: data.object.id }, + }); + + await this.billingSubscriptionItemRepository.upsert( + data.object.items.data.map((item) => { + return { + billingSubscriptionId: billingSubscription.id, + stripeProductId: item.price.product as string, + stripePriceId: item.price.id, + stripeSubscriptionItemId: item.id, + quantity: item.quantity, + }; + }), + { + conflictPaths: ['billingSubscriptionId', 'stripeProductId'], + skipUpdateIfNoValuesChanged: true, + }, + ); + + if ( + data.object.status === SubscriptionStatus.Canceled || + data.object.status === SubscriptionStatus.Unpaid + ) { + await this.workspaceRepository.update(workspaceId, { + activationStatus: WorkspaceActivationStatus.INACTIVE, + }); + } + + if ( + (data.object.status === SubscriptionStatus.Active || + data.object.status === SubscriptionStatus.Trialing || + data.object.status === SubscriptionStatus.PastDue) && + workspace.activationStatus == WorkspaceActivationStatus.INACTIVE + ) { + await this.workspaceRepository.update(workspaceId, { + activationStatus: WorkspaceActivationStatus.ACTIVE, + }); + } + } +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts index 1a4e728fe..c98e8c53b 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts @@ -2,9 +2,12 @@ import { Injectable, Logger } from '@nestjs/common'; import Stripe from 'stripe'; -import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; -import { User } from 'src/engine/core-modules/user/user.entity'; +import { AvailableProduct } from 'src/engine/core-modules/billing/interfaces/available-product.interface'; + +import { ProductPriceEntity } from 'src/engine/core-modules/billing/dto/product-price.entity'; import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity'; +import { User } from 'src/engine/core-modules/user/user.entity'; +import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; @Injectable() export class StripeService { @@ -30,10 +33,28 @@ export class StripeService { ); } - async getProductPrices(stripeProductId: string) { - return this.stripe.prices.search({ + async getStripePrices(product: AvailableProduct) { + const stripeProductId = this.getStripeProductId(product); + + const prices = await this.stripe.prices.search({ query: `product: '${stripeProductId}'`, }); + + return this.formatProductPrices(prices.data); + } + + async getStripePrice(product: AvailableProduct, recurringInterval: string) { + const productPrices = await this.getStripePrices(product); + + return productPrices.find( + (price) => price.recurringInterval === recurringInterval, + ); + } + + getStripeProductId(product: AvailableProduct) { + if (product === AvailableProduct.BasePlan) { + return this.environmentService.get('BILLING_STRIPE_BASE_PLAN_PRODUCT_ID'); + } } async updateSubscriptionItem(stripeItemId: string, quantity: number) { @@ -119,4 +140,29 @@ export class StripeService { }, ); } + + formatProductPrices(prices: Stripe.Price[]) { + const result: Record = {}; + + prices.forEach((item) => { + const interval = item.recurring?.interval; + + if (!interval || !item.unit_amount) { + return; + } + if ( + !result[interval] || + item.created > (result[interval]?.created || 0) + ) { + result[interval] = { + unitAmount: item.unit_amount, + recurringInterval: interval, + created: item.created, + stripePriceId: item.id, + }; + } + }); + + return Object.values(result).sort((a, b) => a.unitAmount - b.unitAmount); + } } diff --git a/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.module.ts b/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.module.ts index a89f24c10..6942c6a55 100644 --- a/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.module.ts +++ b/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.module.ts @@ -3,21 +3,10 @@ import { Module } from '@nestjs/common'; import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; import { OnboardingResolver } from 'src/engine/core-modules/onboarding/onboarding.resolver'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; -import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module'; import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module'; -import { EnvironmentModule } from 'src/engine/integrations/environment/environment.module'; -import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; -import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module'; @Module({ - imports: [ - DataSourceModule, - WorkspaceManagerModule, - UserWorkspaceModule, - EnvironmentModule, - BillingModule, - UserVarsModule, - ], + imports: [BillingModule, UserVarsModule], exports: [OnboardingService], providers: [OnboardingService, OnboardingResolver], }) diff --git a/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.resolver.ts b/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.resolver.ts index 89e7925a2..97fe28e50 100644 --- a/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.resolver.ts @@ -1,13 +1,13 @@ import { UseGuards } from '@nestjs/common'; import { Mutation, Resolver } from '@nestjs/graphql'; -import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; import { OnboardingStepSuccess } from 'src/engine/core-modules/onboarding/dtos/onboarding-step-success.dto'; -import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; -import { User } from 'src/engine/core-modules/user/user.entity'; -import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; +import { User } from 'src/engine/core-modules/user/user.entity'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; +import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; +import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; @UseGuards(JwtAuthGuard) @Resolver() @@ -19,10 +19,11 @@ export class OnboardingResolver { @AuthUser() user: User, @AuthWorkspace() workspace: Workspace, ): Promise { - await this.onboardingService.skipSyncEmailOnboardingStep( - user.id, - workspace.id, - ); + await this.onboardingService.toggleOnboardingConnectAccountCompletion({ + userId: user.id, + workspaceId: workspace.id, + value: true, + }); return { success: true }; } diff --git a/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts b/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts index 0a07f010c..1875924c1 100644 --- a/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts +++ b/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts @@ -1,59 +1,43 @@ -/* eslint-disable @nx/workspace-inject-workspace-repository */ import { Injectable } from '@nestjs/common'; -import { BillingWorkspaceService } from 'src/engine/core-modules/billing/billing.workspace-service'; import { SubscriptionStatus } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; +import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum'; -import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service'; import { User } from 'src/engine/core-modules/user/user.entity'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; -import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; -import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service'; -import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository'; -import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; -import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; +import { WorkspaceActivationStatus } from 'src/engine/core-modules/workspace/workspace.entity'; +import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { isDefined } from 'src/utils/is-defined'; -enum OnboardingStepValues { - SKIPPED = 'SKIPPED', +export enum OnboardingStepKeys { + ONBOARDING_CONNECT_ACCOUNT_COMPLETE = 'ONBOARDING_CONNECT_ACCOUNT_COMPLETE', + ONBOARDING_INVITE_TEAM_COMPLETE = 'ONBOARDING_INVITE_TEAM_COMPLETE', + ONBOARDING_CREATE_PROFILE_COMPLETE = 'ONBOARDING_CREATE_PROFILE_COMPLETE', } -enum OnboardingStepKeys { - SYNC_EMAIL_ONBOARDING_STEP = 'SYNC_EMAIL_ONBOARDING_STEP', - INVITE_TEAM_ONBOARDING_STEP = 'INVITE_TEAM_ONBOARDING_STEP', -} - -type OnboardingKeyValueTypeMap = { - [OnboardingStepKeys.SYNC_EMAIL_ONBOARDING_STEP]: OnboardingStepValues; - [OnboardingStepKeys.INVITE_TEAM_ONBOARDING_STEP]: OnboardingStepValues; +export type OnboardingKeyValueTypeMap = { + [OnboardingStepKeys.ONBOARDING_CONNECT_ACCOUNT_COMPLETE]: boolean; + [OnboardingStepKeys.ONBOARDING_INVITE_TEAM_COMPLETE]: boolean; + [OnboardingStepKeys.ONBOARDING_CREATE_PROFILE_COMPLETE]: boolean; }; @Injectable() export class OnboardingService { constructor( - private readonly twentyORMManager: TwentyORMManager, - private readonly billingWorkspaceService: BillingWorkspaceService, - private readonly workspaceManagerService: WorkspaceManagerService, - private readonly userWorkspaceService: UserWorkspaceService, + private readonly billingSubscriptionService: BillingSubscriptionService, + private readonly environmentService: EnvironmentService, private readonly userVarsService: UserVarsService, - @InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity) - private readonly connectedAccountRepository: ConnectedAccountRepository, ) {} private async isSubscriptionIncompleteOnboardingStatus(user: User) { - const isBillingEnabledForWorkspace = - await this.billingWorkspaceService.isBillingEnabledForWorkspace( - user.defaultWorkspaceId, - ); + const isBillingEnabled = this.environmentService.get('IS_BILLING_ENABLED'); - if (!isBillingEnabledForWorkspace) { + if (!isBillingEnabled) { return false; } const currentBillingSubscription = - await this.billingWorkspaceService.getCurrentBillingSubscription({ + await this.billingSubscriptionService.getCurrentBillingSubscription({ workspaceId: user.defaultWorkspaceId, }); @@ -64,57 +48,9 @@ export class OnboardingService { } private async isWorkspaceActivationOnboardingStatus(user: User) { - return !(await this.workspaceManagerService.doesDataSourceExist( - user.defaultWorkspaceId, - )); - } - - private async isProfileCreationOnboardingStatus(user: User) { - const workspaceMemberRepository = - await this.twentyORMManager.getRepository( - 'workspaceMember', - ); - - const workspaceMember = await workspaceMemberRepository.findOneBy({ - userId: user.id, - }); - return ( - workspaceMember && - (!workspaceMember.name.firstName || !workspaceMember.name.lastName) - ); - } - - private async isSyncEmailOnboardingStatus(user: User) { - const syncEmailValue = await this.userVarsService.get({ - userId: user.id, - workspaceId: user.defaultWorkspaceId, - key: OnboardingStepKeys.SYNC_EMAIL_ONBOARDING_STEP, - }); - const isSyncEmailSkipped = syncEmailValue === OnboardingStepValues.SKIPPED; - const connectedAccounts = - await this.connectedAccountRepository.getAllByUserId( - user.id, - user.defaultWorkspaceId, - ); - - return !isSyncEmailSkipped && !connectedAccounts?.length; - } - - private async isInviteTeamOnboardingStatus(workspace: Workspace) { - const inviteTeamValue = await this.userVarsService.get({ - workspaceId: workspace.id, - key: OnboardingStepKeys.INVITE_TEAM_ONBOARDING_STEP, - }); - const isInviteTeamSkipped = - inviteTeamValue === OnboardingStepValues.SKIPPED; - const workspaceMemberCount = await this.userWorkspaceService.getUserCount( - workspace.id, - ); - - return ( - !isInviteTeamSkipped && - (!workspaceMemberCount || workspaceMemberCount <= 1) + user.defaultWorkspace.activationStatus === + WorkspaceActivationStatus.PENDING_CREATION ); } @@ -127,35 +63,82 @@ export class OnboardingService { return OnboardingStatus.WORKSPACE_ACTIVATION; } - if (await this.isProfileCreationOnboardingStatus(user)) { + const userVars = await this.userVarsService.getAll({ + userId: user.id, + workspaceId: user.defaultWorkspaceId, + }); + + const isProfileCreationComplete = + userVars.get(OnboardingStepKeys.ONBOARDING_CREATE_PROFILE_COMPLETE) === + true; + + const isConnectAccountComplete = + userVars.get(OnboardingStepKeys.ONBOARDING_CONNECT_ACCOUNT_COMPLETE) === + true; + + const isInviteTeamComplete = + userVars.get(OnboardingStepKeys.ONBOARDING_INVITE_TEAM_COMPLETE) === true; + + if (!isProfileCreationComplete) { return OnboardingStatus.PROFILE_CREATION; } - if (await this.isSyncEmailOnboardingStatus(user)) { + if (!isConnectAccountComplete) { return OnboardingStatus.SYNC_EMAIL; } - if (await this.isInviteTeamOnboardingStatus(user.defaultWorkspace)) { + if (!isInviteTeamComplete) { return OnboardingStatus.INVITE_TEAM; } return OnboardingStatus.COMPLETED; } - async skipInviteTeamOnboardingStep(workspaceId: string) { + async toggleOnboardingConnectAccountCompletion({ + userId, + workspaceId, + value, + }: { + userId: string; + workspaceId: string; + value: boolean; + }) { await this.userVarsService.set({ - workspaceId, - key: OnboardingStepKeys.INVITE_TEAM_ONBOARDING_STEP, - value: OnboardingStepValues.SKIPPED, + userId, + workspaceId: workspaceId, + key: OnboardingStepKeys.ONBOARDING_CONNECT_ACCOUNT_COMPLETE, + value, }); } - async skipSyncEmailOnboardingStep(userId: string, workspaceId: string) { + async toggleOnboardingInviteTeamCompletion({ + workspaceId, + value, + }: { + workspaceId: string; + value: boolean; + }) { + await this.userVarsService.set({ + workspaceId, + key: OnboardingStepKeys.ONBOARDING_INVITE_TEAM_COMPLETE, + value, + }); + } + + async toggleOnboardingCreateProfileCompletion({ + userId, + workspaceId, + value, + }: { + userId: string; + workspaceId: string; + value: boolean; + }) { await this.userVarsService.set({ userId, workspaceId, - key: OnboardingStepKeys.SYNC_EMAIL_ONBOARDING_STEP, - value: OnboardingStepValues.SKIPPED, + key: OnboardingStepKeys.ONBOARDING_CREATE_PROFILE_COMPLETE, + value, }); } } 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 821e2e65c..671977148 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 @@ -37,6 +37,7 @@ export class UserWorkspaceService extends TypeOrmQueryService { payload.workspaceId = workspaceId; payload.userId = userId; + payload.name = 'user.signup'; this.eventEmitter.emit('user.signup', payload); @@ -80,6 +81,7 @@ export class UserWorkspaceService extends TypeOrmQueryService { after: workspaceMember[0], }; payload.recordId = workspaceMember[0].id; + payload.name = 'workspaceMember.created'; this.eventEmitter.emit('workspaceMember.created', payload); } 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 161107603..e8aef7fe8 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 @@ -18,6 +18,7 @@ import { DataSourceService } from 'src/engine/metadata-modules/data-source/data- import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; +// eslint-disable-next-line @nx/workspace-inject-workspace-repository export class UserService extends TypeOrmQueryService { constructor( @InjectRepository(User, 'core') @@ -115,6 +116,7 @@ export class UserService extends TypeOrmQueryService { }; payload.name = 'workspaceMember.deleted'; payload.recordId = workspaceMember.id; + payload.name = 'workspaceMember.deleted'; this.eventEmitter.emit('workspaceMember.deleted', payload); diff --git a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts index 696287311..4f3bd8008 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts @@ -1,16 +1,16 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service'; +import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; +import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; -import { User } from 'src/engine/core-modules/user/user.entity'; -import { BillingWorkspaceService } from 'src/engine/core-modules/billing/billing.workspace-service'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { UserService } from 'src/engine/core-modules/user/services/user.service'; +import { User } from 'src/engine/core-modules/user/user.entity'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { EmailService } from 'src/engine/integrations/email/email.service'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; -import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; +import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service'; import { WorkspaceService } from './workspace.service'; @@ -46,7 +46,7 @@ describe('WorkspaceService', () => { useValue: {}, }, { - provide: BillingWorkspaceService, + provide: BillingSubscriptionService, useValue: {}, }, { 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 28a24059b..b31b8a2f9 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 @@ -8,7 +8,7 @@ import { render } from '@react-email/render'; import { SendInviteLinkEmail } from 'twenty-emails'; import { Repository } from 'typeorm'; -import { BillingWorkspaceService } from 'src/engine/core-modules/billing/billing.workspace-service'; +import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; @@ -23,6 +23,7 @@ import { EmailService } from 'src/engine/integrations/email/email.service'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service'; +// eslint-disable-next-line @nx/workspace-inject-workspace-repository export class WorkspaceService extends TypeOrmQueryService { constructor( @InjectRepository(Workspace, 'core') @@ -33,7 +34,7 @@ export class WorkspaceService extends TypeOrmQueryService { private readonly userWorkspaceRepository: Repository, private readonly workspaceManagerService: WorkspaceManagerService, private readonly userWorkspaceService: UserWorkspaceService, - private readonly billingWorkspaceService: BillingWorkspaceService, + private readonly billingSubscriptionService: BillingSubscriptionService, private readonly environmentService: EnvironmentService, private readonly emailService: EmailService, private readonly onboardingService: OnboardingService, @@ -65,7 +66,7 @@ export class WorkspaceService extends TypeOrmQueryService { assert(workspace, 'Workspace not found'); await this.userWorkspaceRepository.delete({ workspaceId: id }); - await this.billingWorkspaceService.deleteSubscription(workspace.id); + await this.billingSubscriptionService.deleteSubscription(workspace.id); await this.workspaceManagerService.delete(id); @@ -99,7 +100,6 @@ export class WorkspaceService extends TypeOrmQueryService { userId, workspaceId, }); - await this.onboardingService.skipInviteTeamOnboardingStep(workspaceId); await this.reassignOrRemoveUserDefaultWorkspace(workspaceId, userId); } @@ -142,7 +142,10 @@ export class WorkspaceService extends TypeOrmQueryService { }); } - await this.onboardingService.skipInviteTeamOnboardingStep(workspace.id); + await this.onboardingService.toggleOnboardingInviteTeamCompletion({ + workspaceId: workspace.id, + value: true, + }); return { success: true }; } diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace-workspace-member.listener.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace-workspace-member.listener.ts index 240b4eb1c..324c0d929 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace-workspace-member.listener.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace-workspace-member.listener.ts @@ -1,23 +1,51 @@ import { Injectable } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; -import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; -import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; -import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; -import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event'; +import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; import { HandleWorkspaceMemberDeletedJob, HandleWorkspaceMemberDeletedJobData, } from 'src/engine/core-modules/workspace/handle-workspace-member-deleted.job'; +import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event'; +import { ObjectRecordUpdateEvent } from 'src/engine/integrations/event-emitter/types/object-record-update.event'; import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator'; +import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; +import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; +import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @Injectable() export class WorkspaceWorkspaceMemberListener { constructor( + private readonly onboardingService: OnboardingService, @InjectMessageQueue(MessageQueue.workspaceQueue) private readonly messageQueueService: MessageQueueService, ) {} + @OnEvent('workspaceMember.updated') + async handleUpdateEvent( + payload: ObjectRecordUpdateEvent, + ) { + const { firstName: firstNameBefore, lastName: lastNameBefore } = + payload.properties.before.name; + + const { firstName: firstNameAfter, lastName: lastNameAfter } = + payload.properties.after.name; + + if (firstNameAfter === '' && lastNameAfter === '') { + return; + } + + if (!payload.userId) { + return; + } + + await this.onboardingService.toggleOnboardingCreateProfileCompletion({ + userId: payload.userId, + workspaceId: payload.workspaceId, + value: true, + }); + } + @OnEvent('workspaceMember.deleted') async handleDeleteEvent( payload: ObjectRecordDeleteEvent, 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 5e9668cab..213427eaf 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 @@ -17,6 +17,7 @@ import { WorkspaceResolver } from 'src/engine/core-modules/workspace/workspace.r import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module'; import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module'; +import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module'; import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts'; import { Workspace } from './workspace.entity'; 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 dff5ed2ee..3722d5457 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 @@ -12,8 +12,8 @@ import { FileUpload, GraphQLUpload } from 'graphql-upload'; import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface'; -import { BillingWorkspaceService } from 'src/engine/core-modules/billing/billing.workspace-service'; import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; +import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { User } from 'src/engine/core-modules/user/user.entity'; @@ -41,7 +41,7 @@ export class WorkspaceResolver { private readonly workspaceCacheVersionService: WorkspaceCacheVersionService, private readonly userWorkspaceService: UserWorkspaceService, private readonly fileUploadService: FileUploadService, - private readonly billingWorkspaceService: BillingWorkspaceService, + private readonly billingSubscriptionService: BillingSubscriptionService, ) {} @Query(() => Workspace) @@ -112,7 +112,7 @@ export class WorkspaceResolver { async currentBillingSubscription( @Parent() workspace: Workspace, ): Promise { - return this.billingWorkspaceService.getCurrentBillingSubscription({ + return this.billingSubscriptionService.getCurrentBillingSubscription({ workspaceId: workspace.id, }); } diff --git a/packages/twenty-server/src/engine/workspace-manager/demo-objects-prefill-data/company.ts b/packages/twenty-server/src/engine/workspace-manager/demo-objects-prefill-data/company.ts index b29a09e00..905279ab4 100644 --- a/packages/twenty-server/src/engine/workspace-manager/demo-objects-prefill-data/company.ts +++ b/packages/twenty-server/src/engine/workspace-manager/demo-objects-prefill-data/company.ts @@ -15,10 +15,10 @@ export const companyPrefillDemoData = async ( 'addressAddressCity', 'employees', 'linkedinLinkPrimaryLinkUrl', - 'position', 'createdBySource', 'createdByWorkspaceMemberId', - 'createdByName' + 'createdByName', + 'position' ]) .orIgnore() .values( diff --git a/packages/twenty-server/src/engine/workspace-manager/demo-objects-prefill-data/opportunity.ts b/packages/twenty-server/src/engine/workspace-manager/demo-objects-prefill-data/opportunity.ts index 88ec9f3f9..99448418f 100644 --- a/packages/twenty-server/src/engine/workspace-manager/demo-objects-prefill-data/opportunity.ts +++ b/packages/twenty-server/src/engine/workspace-manager/demo-objects-prefill-data/opportunity.ts @@ -1,3 +1,4 @@ +import { DEMO_SEED_WORKSPACE_MEMBER_IDS } from 'src/engine/workspace-manager/demo-objects-prefill-data/workspace-member'; import { EntityManager } from 'typeorm'; import { v4 } from 'uuid'; @@ -25,6 +26,9 @@ const generateOpportunities = (companies) => { stage: getRandomStage(), pointOfContactId: company.personId, companyId: company.id, + createdBySource: 'MANUAL', + createdByWorkspaceMemberId: DEMO_SEED_WORKSPACE_MEMBER_IDS.NOAH, + createdByName: 'Noah A', })); }; @@ -53,6 +57,9 @@ export const opportunityPrefillDemoData = async ( 'stage', 'pointOfContactId', 'companyId', + 'createdBySource', + 'createdByWorkspaceMemberId', + 'createdByName', 'position', ]) .orIgnore() diff --git a/packages/twenty-server/src/engine/workspace-manager/demo-objects-prefill-data/person.ts b/packages/twenty-server/src/engine/workspace-manager/demo-objects-prefill-data/person.ts index 190713107..c12b0b201 100644 --- a/packages/twenty-server/src/engine/workspace-manager/demo-objects-prefill-data/person.ts +++ b/packages/twenty-server/src/engine/workspace-manager/demo-objects-prefill-data/person.ts @@ -19,11 +19,11 @@ export const personPrefillDemoData = async ( jobTitle: person.jobTitle, city: person.city, avatarUrl: person.avatarUrl, - position: index, companyId: companies[Math.floor(index / 2)].id, createdBySource: person.createdBySource, createdByWorkspaceMemberId: person.createdByWorkspaceMemberId, - createdByName: person.createdByName + createdByName: person.createdByName, + position: index })); await entityManager @@ -37,11 +37,11 @@ export const personPrefillDemoData = async ( 'jobTitle', 'city', 'avatarUrl', - 'position', 'companyId', 'createdBySource', 'createdByWorkspaceMemberId', 'createdByName', + 'position', ]) .orIgnore() .values(people) diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts index 9fafad7c8..27ec9ca9f 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts @@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; @@ -44,7 +45,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta WorkspaceMemberWorkspaceEntity, ]), CalendarEventParticipantManagerModule, - TypeOrmModule.forFeature([FeatureFlagEntity], 'core'), + TypeOrmModule.forFeature([FeatureFlagEntity, Workspace], 'core'), TypeOrmModule.forFeature([DataSourceEntity], 'metadata'), WorkspaceDataSourceModule, CalendarEventCleanerModule, diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-event-list-fetch.cron.job.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-event-list-fetch.cron.job.ts index 09fdc599a..87d58c3f6 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-event-list-fetch.cron.job.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/crons/jobs/calendar-event-list-fetch.cron.job.ts @@ -1,14 +1,16 @@ import { InjectRepository } from '@nestjs/typeorm'; -import { Any, In, Repository } from 'typeorm'; +import { Any, Repository } from 'typeorm'; -import { BillingService } from 'src/engine/core-modules/billing/billing.service'; +import { + Workspace, + WorkspaceActivationStatus, +} from 'src/engine/core-modules/workspace/workspace.entity'; import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator'; import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; -import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { CalendarEventListFetchJob, @@ -21,33 +23,25 @@ import { CalendarChannelSyncStage } from 'src/modules/calendar/common/standard-o }) export class CalendarEventListFetchCronJob { constructor( - @InjectRepository(DataSourceEntity, 'metadata') - private readonly dataSourceRepository: Repository, + @InjectRepository(Workspace, 'core') + private readonly workspaceRepository: Repository, @InjectMessageQueue(MessageQueue.calendarQueue) private readonly messageQueueService: MessageQueueService, - private readonly billingService: BillingService, private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} @Process(CalendarEventListFetchCronJob.name) async handle(): Promise { - const workspaceIds = - await this.billingService.getActiveSubscriptionWorkspaceIds(); - - const dataSources = await this.dataSourceRepository.find({ + const activeWorkspaces = await this.workspaceRepository.find({ where: { - workspaceId: In(workspaceIds), + activationStatus: WorkspaceActivationStatus.ACTIVE, }, }); - const workspaceIdsWithDataSources = new Set( - dataSources.map((dataSource) => dataSource.workspaceId), - ); - - for (const workspaceId of workspaceIdsWithDataSources) { + for (const activeWorkspace of activeWorkspaces) { const calendarChannelRepository = await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, + activeWorkspace.id, 'calendarChannel', ); @@ -66,7 +60,7 @@ export class CalendarEventListFetchCronJob { CalendarEventListFetchJob.name, { calendarChannelId: calendarChannel.id, - workspaceId, + workspaceId: activeWorkspace.id, }, ); } diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts index ad0f4e11d..4ea030dcf 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts @@ -141,6 +141,7 @@ export class CalendarSaveEventsService { this.eventEmitter.emit(`calendarEventParticipant.matched`, { workspaceId, + name: 'calendarEventParticipant.matched', workspaceMemberId: connectedAccount.accountOwnerId, calendarEventParticipants: savedCalendarEventParticipantsToEmit, }); diff --git a/packages/twenty-server/src/modules/match-participant/match-participant.service.ts b/packages/twenty-server/src/modules/match-participant/match-participant.service.ts index 6ad2f5f99..d7bba2e4b 100644 --- a/packages/twenty-server/src/modules/match-participant/match-participant.service.ts +++ b/packages/twenty-server/src/modules/match-participant/match-participant.service.ts @@ -157,6 +157,7 @@ export class MatchParticipantService< this.eventEmitter.emit(`${objectMetadataName}.matched`, { workspaceId, + name: `${objectMetadataName}.matched`, workspaceMemberId: null, participants: updatedParticipants, }); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-message-list-fetch.cron.job.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-message-list-fetch.cron.job.ts index 0e8811b6b..7e6f42d97 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-message-list-fetch.cron.job.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-message-list-fetch.cron.job.ts @@ -1,9 +1,15 @@ import { Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, In } from 'typeorm'; +import { Repository } from 'typeorm'; -import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; +import { + Workspace, + WorkspaceActivationStatus, +} from 'src/engine/core-modules/workspace/workspace.entity'; +import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator'; +import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; +import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; @@ -13,46 +19,35 @@ import { MessageChannelWorkspaceEntity, } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { - MessagingMessageListFetchJobData, MessagingMessageListFetchJob, + MessagingMessageListFetchJobData, } from 'src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job'; -import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator'; -import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; -import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; -import { BillingService } from 'src/engine/core-modules/billing/billing.service'; @Processor(MessageQueue.cronQueue) export class MessagingMessageListFetchCronJob { private readonly logger = new Logger(MessagingMessageListFetchCronJob.name); constructor( - @InjectRepository(DataSourceEntity, 'metadata') - private readonly dataSourceRepository: Repository, + @InjectRepository(Workspace, 'core') + private readonly workspaceRepository: Repository, @InjectMessageQueue(MessageQueue.messagingQueue) private readonly messageQueueService: MessageQueueService, @InjectObjectMetadataRepository(MessageChannelWorkspaceEntity) private readonly messageChannelRepository: MessageChannelRepository, - private readonly billingService: BillingService, ) {} @Process(MessagingMessageListFetchCronJob.name) async handle(): Promise { - const workspaceIds = - await this.billingService.getActiveSubscriptionWorkspaceIds(); - - const dataSources = await this.dataSourceRepository.find({ + const activeWorkspaces = await this.workspaceRepository.find({ where: { - workspaceId: In(workspaceIds), + activationStatus: WorkspaceActivationStatus.ACTIVE, }, }); - const workspaceIdsWithDataSources = new Set( - dataSources.map((dataSource) => dataSource.workspaceId), - ); - - for (const workspaceId of workspaceIdsWithDataSources) { - const messageChannels = - await this.messageChannelRepository.getAll(workspaceId); + for (const activeWorkspace of activeWorkspaces) { + const messageChannels = await this.messageChannelRepository.getAll( + activeWorkspace.id, + ); for (const messageChannel of messageChannels) { if ( @@ -65,7 +60,7 @@ export class MessagingMessageListFetchCronJob { await this.messageQueueService.add( MessagingMessageListFetchJob.name, { - workspaceId, + workspaceId: activeWorkspace.id, messageChannelId: messageChannel.id, }, ); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-messages-import.cron.job.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-messages-import.cron.job.ts index 4be6ce2e1..e47e49d85 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-messages-import.cron.job.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-messages-import.cron.job.ts @@ -1,58 +1,53 @@ import { Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, In } from 'typeorm'; +import { Repository } from 'typeorm'; -import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; +import { + Workspace, + WorkspaceActivationStatus, +} from 'src/engine/core-modules/workspace/workspace.entity'; +import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator'; +import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; +import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; -import { - MessagingMessagesImportJobData, - MessagingMessagesImportJob, -} from 'src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job'; -import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; -import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; -import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator'; -import { MessageChannelRepository } from 'src/modules/messaging/common/repositories/message-channel.repository'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; +import { MessageChannelRepository } from 'src/modules/messaging/common/repositories/message-channel.repository'; import { MessageChannelSyncStage, MessageChannelWorkspaceEntity, } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; -import { BillingService } from 'src/engine/core-modules/billing/billing.service'; +import { + MessagingMessagesImportJob, + MessagingMessagesImportJobData, +} from 'src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job'; @Processor(MessageQueue.cronQueue) export class MessagingMessagesImportCronJob { private readonly logger = new Logger(MessagingMessagesImportCronJob.name); constructor( - @InjectRepository(DataSourceEntity, 'metadata') - private readonly dataSourceRepository: Repository, + @InjectRepository(Workspace, 'core') + private readonly workspaceRepository: Repository, @InjectMessageQueue(MessageQueue.messagingQueue) private readonly messageQueueService: MessageQueueService, @InjectObjectMetadataRepository(MessageChannelWorkspaceEntity) private readonly messageChannelRepository: MessageChannelRepository, - private readonly billingService: BillingService, ) {} @Process(MessagingMessagesImportCronJob.name) async handle(): Promise { - const workspaceIds = - await this.billingService.getActiveSubscriptionWorkspaceIds(); - - const dataSources = await this.dataSourceRepository.find({ + const activeWorkspaces = await this.workspaceRepository.find({ where: { - workspaceId: In(workspaceIds), + activationStatus: WorkspaceActivationStatus.ACTIVE, }, }); - const workspaceIdsWithDataSources = new Set( - dataSources.map((dataSource) => dataSource.workspaceId), - ); - - for (const workspaceId of workspaceIdsWithDataSources) { - const messageChannels = - await this.messageChannelRepository.getAll(workspaceId); + for (const activeWorkspace of activeWorkspaces) { + const messageChannels = await this.messageChannelRepository.getAll( + activeWorkspace.id, + ); for (const messageChannel of messageChannels) { if ( @@ -63,7 +58,7 @@ export class MessagingMessagesImportCronJob { await this.messageQueueService.add( MessagingMessagesImportJob.name, { - workspaceId, + workspaceId: activeWorkspace.id, messageChannelId: messageChannel.id, }, ); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-ongoing-stale.cron.job.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-ongoing-stale.cron.job.ts index bc1bce31c..d2c3218b0 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-ongoing-stale.cron.job.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/crons/jobs/messaging-ongoing-stale.cron.job.ts @@ -1,49 +1,43 @@ import { InjectRepository } from '@nestjs/typeorm'; -import { Repository, In } from 'typeorm'; +import { Repository } from 'typeorm'; -import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; -import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; -import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; -import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; +import { + Workspace, + WorkspaceActivationStatus, +} from 'src/engine/core-modules/workspace/workspace.entity'; import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator'; +import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; +import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; +import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; import { - MessagingOngoingStaleJobData, MessagingOngoingStaleJob, + MessagingOngoingStaleJobData, } from 'src/modules/messaging/message-import-manager/jobs/messaging-ongoing-stale.job'; -import { BillingService } from 'src/engine/core-modules/billing/billing.service'; @Processor(MessageQueue.cronQueue) export class MessagingOngoingStaleCronJob { constructor( - @InjectRepository(DataSourceEntity, 'metadata') - private readonly dataSourceRepository: Repository, + @InjectRepository(Workspace, 'core') + private readonly workspaceRepository: Repository, @InjectMessageQueue(MessageQueue.messagingQueue) private readonly messageQueueService: MessageQueueService, - private readonly billingService: BillingService, ) {} @Process(MessagingOngoingStaleCronJob.name) async handle(): Promise { - const workspaceIds = - await this.billingService.getActiveSubscriptionWorkspaceIds(); - - const dataSources = await this.dataSourceRepository.find({ + const activeWorkspaces = await this.workspaceRepository.find({ where: { - workspaceId: In(workspaceIds), + activationStatus: WorkspaceActivationStatus.ACTIVE, }, }); - const workspaceIdsWithDataSources = new Set( - dataSources.map((dataSource) => dataSource.workspaceId), - ); - - for (const workspaceId of workspaceIdsWithDataSources) { + for (const activeWorkspace of activeWorkspaces) { await this.messageQueueService.add( MessagingOngoingStaleJob.name, { - workspaceId, + workspaceId: activeWorkspace.id, }, ); } diff --git a/packages/twenty-server/src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron.ts b/packages/twenty-server/src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron.ts index 273d7ddf3..fb7561d20 100644 --- a/packages/twenty-server/src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron.ts +++ b/packages/twenty-server/src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron.ts @@ -2,14 +2,15 @@ import { Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import snakeCase from 'lodash.snakecase'; -import { In, Repository } from 'typeorm'; +import { Repository } from 'typeorm'; -import { BillingService } from 'src/engine/core-modules/billing/billing.service'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { + Workspace, + WorkspaceActivationStatus, +} from 'src/engine/core-modules/workspace/workspace.entity'; import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; -import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { MessageChannelRepository } from 'src/modules/messaging/common/repositories/message-channel.repository'; import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; @@ -24,11 +25,8 @@ export class MessagingMessageChannelSyncStatusMonitoringCronJob { constructor( @InjectRepository(Workspace, 'core') private readonly workspaceRepository: Repository, - @InjectRepository(DataSourceEntity, 'metadata') - private readonly dataSourceRepository: Repository, @InjectObjectMetadataRepository(MessageChannelWorkspaceEntity) private readonly messageChannelRepository: MessageChannelRepository, - private readonly billingService: BillingService, private readonly messagingTelemetryService: MessagingTelemetryService, ) {} @@ -41,22 +39,16 @@ export class MessagingMessageChannelSyncStatusMonitoringCronJob { message: 'Starting message channel sync status monitoring', }); - const workspaceIds = - await this.billingService.getActiveSubscriptionWorkspaceIds(); - - const dataSources = await this.dataSourceRepository.find({ + const activeWorkspaces = await this.workspaceRepository.find({ where: { - workspaceId: In(workspaceIds), + activationStatus: WorkspaceActivationStatus.ACTIVE, }, }); - const workspaceIdsWithDataSources = new Set( - dataSources.map((dataSource) => dataSource.workspaceId), - ); - - for (const workspaceId of workspaceIdsWithDataSources) { - const messageChannels = - await this.messageChannelRepository.getAll(workspaceId); + for (const activeWorkspace of activeWorkspaces) { + const messageChannels = await this.messageChannelRepository.getAll( + activeWorkspace.id, + ); for (const messageChannel of messageChannels) { if (!messageChannel.syncStatus) { @@ -66,7 +58,7 @@ export class MessagingMessageChannelSyncStatusMonitoringCronJob { eventName: `message_channel.monitoring.sync_status.${snakeCase( messageChannel.syncStatus, )}`, - workspaceId, + workspaceId: activeWorkspace.id, connectedAccountId: messageChannel.connectedAccountId, messageChannelId: messageChannel.id, message: messageChannel.syncStatus,