Fix onboarding status performance issues (#6512)
Updated the onboardingStatus computation to improve performances --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -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
|
||||
};};
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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: {},
|
||||
},
|
||||
],
|
||||
|
||||
@ -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<User>,
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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 {}
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
@ -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<BillingSubscription>,
|
||||
@InjectRepository(FeatureFlagEntity, 'core')
|
||||
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
) {}
|
||||
|
||||
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,
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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<BillingSubscription>,
|
||||
@InjectRepository(FeatureFlagEntity, 'core')
|
||||
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
|
||||
@InjectRepository(BillingSubscriptionItem, 'core')
|
||||
private readonly billingSubscriptionItemRepository: Repository<BillingSubscriptionItem>,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
) {}
|
||||
|
||||
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<string, ProductPriceEntity> = {};
|
||||
|
||||
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<string> {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
export enum AvailableProduct {
|
||||
BasePlan = 'base-plan',
|
||||
}
|
||||
@ -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,
|
||||
);
|
||||
|
||||
|
||||
@ -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<WorkspaceMemberWorkspaceEntity>,
|
||||
) {
|
||||
const isBillingEnabledForWorkspace =
|
||||
await this.billingWorkspaceService.isBillingEnabledForWorkspace(
|
||||
payload.workspaceId,
|
||||
);
|
||||
|
||||
if (!isBillingEnabledForWorkspace) {
|
||||
if (!this.environmentService.get('IS_BILLING_ENABLED')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -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<BillingSubscription>,
|
||||
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||
) {}
|
||||
|
||||
async computeCheckoutSessionURL(
|
||||
user: User,
|
||||
workspace: Workspace,
|
||||
priceId: string,
|
||||
successUrlPath?: string,
|
||||
): Promise<string> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -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<BillingSubscription>,
|
||||
@InjectRepository(FeatureFlagEntity, 'core')
|
||||
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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<BillingSubscription>,
|
||||
@InjectRepository(BillingSubscriptionItem, 'core')
|
||||
private readonly billingSubscriptionItemRepository: Repository<BillingSubscriptionItem>,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
) {}
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<string, ProductPriceEntity> = {};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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],
|
||||
})
|
||||
|
||||
@ -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<OnboardingStepSuccess> {
|
||||
await this.onboardingService.skipSyncEmailOnboardingStep(
|
||||
user.id,
|
||||
workspace.id,
|
||||
);
|
||||
await this.onboardingService.toggleOnboardingConnectAccountCompletion({
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
value: true,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@ -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<OnboardingKeyValueTypeMap>,
|
||||
@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<WorkspaceMemberWorkspaceEntity>(
|
||||
'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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,6 +37,7 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
|
||||
|
||||
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<UserWorkspace> {
|
||||
after: workspaceMember[0],
|
||||
};
|
||||
payload.recordId = workspaceMember[0].id;
|
||||
payload.name = 'workspaceMember.created';
|
||||
|
||||
this.eventEmitter.emit('workspaceMember.created', payload);
|
||||
}
|
||||
|
||||
@ -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<User> {
|
||||
constructor(
|
||||
@InjectRepository(User, 'core')
|
||||
@ -115,6 +116,7 @@ export class UserService extends TypeOrmQueryService<User> {
|
||||
};
|
||||
payload.name = 'workspaceMember.deleted';
|
||||
payload.recordId = workspaceMember.id;
|
||||
payload.name = 'workspaceMember.deleted';
|
||||
|
||||
this.eventEmitter.emit('workspaceMember.deleted', payload);
|
||||
|
||||
|
||||
@ -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: {},
|
||||
},
|
||||
{
|
||||
|
||||
@ -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<Workspace> {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
@ -33,7 +34,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
||||
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
|
||||
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<Workspace> {
|
||||
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<Workspace> {
|
||||
userId,
|
||||
workspaceId,
|
||||
});
|
||||
await this.onboardingService.skipInviteTeamOnboardingStep(workspaceId);
|
||||
await this.reassignOrRemoveUserDefaultWorkspace(workspaceId, userId);
|
||||
}
|
||||
|
||||
@ -142,7 +142,10 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
||||
});
|
||||
}
|
||||
|
||||
await this.onboardingService.skipInviteTeamOnboardingStep(workspace.id);
|
||||
await this.onboardingService.toggleOnboardingInviteTeamCompletion({
|
||||
workspaceId: workspace.id,
|
||||
value: true,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@ -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<WorkspaceMemberWorkspaceEntity>,
|
||||
) {
|
||||
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<WorkspaceMemberWorkspaceEntity>,
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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<BillingSubscription | null> {
|
||||
return this.billingWorkspaceService.getCurrentBillingSubscription({
|
||||
return this.billingSubscriptionService.getCurrentBillingSubscription({
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
@ -15,10 +15,10 @@ export const companyPrefillDemoData = async (
|
||||
'addressAddressCity',
|
||||
'employees',
|
||||
'linkedinLinkPrimaryLinkUrl',
|
||||
'position',
|
||||
'createdBySource',
|
||||
'createdByWorkspaceMemberId',
|
||||
'createdByName'
|
||||
'createdByName',
|
||||
'position'
|
||||
])
|
||||
.orIgnore()
|
||||
.values(
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<DataSourceEntity>,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectMessageQueue(MessageQueue.calendarQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
private readonly billingService: BillingService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
@Process(CalendarEventListFetchCronJob.name)
|
||||
async handle(): Promise<void> {
|
||||
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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -141,6 +141,7 @@ export class CalendarSaveEventsService {
|
||||
|
||||
this.eventEmitter.emit(`calendarEventParticipant.matched`, {
|
||||
workspaceId,
|
||||
name: 'calendarEventParticipant.matched',
|
||||
workspaceMemberId: connectedAccount.accountOwnerId,
|
||||
calendarEventParticipants: savedCalendarEventParticipantsToEmit,
|
||||
});
|
||||
|
||||
@ -157,6 +157,7 @@ export class MatchParticipantService<
|
||||
|
||||
this.eventEmitter.emit(`${objectMetadataName}.matched`, {
|
||||
workspaceId,
|
||||
name: `${objectMetadataName}.matched`,
|
||||
workspaceMemberId: null,
|
||||
participants: updatedParticipants,
|
||||
});
|
||||
|
||||
@ -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<DataSourceEntity>,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectMessageQueue(MessageQueue.messagingQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
@InjectObjectMetadataRepository(MessageChannelWorkspaceEntity)
|
||||
private readonly messageChannelRepository: MessageChannelRepository,
|
||||
private readonly billingService: BillingService,
|
||||
) {}
|
||||
|
||||
@Process(MessagingMessageListFetchCronJob.name)
|
||||
async handle(): Promise<void> {
|
||||
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<MessagingMessageListFetchJobData>(
|
||||
MessagingMessageListFetchJob.name,
|
||||
{
|
||||
workspaceId,
|
||||
workspaceId: activeWorkspace.id,
|
||||
messageChannelId: messageChannel.id,
|
||||
},
|
||||
);
|
||||
|
||||
@ -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<DataSourceEntity>,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectMessageQueue(MessageQueue.messagingQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
@InjectObjectMetadataRepository(MessageChannelWorkspaceEntity)
|
||||
private readonly messageChannelRepository: MessageChannelRepository,
|
||||
private readonly billingService: BillingService,
|
||||
) {}
|
||||
|
||||
@Process(MessagingMessagesImportCronJob.name)
|
||||
async handle(): Promise<void> {
|
||||
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<MessagingMessagesImportJobData>(
|
||||
MessagingMessagesImportJob.name,
|
||||
{
|
||||
workspaceId,
|
||||
workspaceId: activeWorkspace.id,
|
||||
messageChannelId: messageChannel.id,
|
||||
},
|
||||
);
|
||||
|
||||
@ -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<DataSourceEntity>,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
private readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectMessageQueue(MessageQueue.messagingQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
private readonly billingService: BillingService,
|
||||
) {}
|
||||
|
||||
@Process(MessagingOngoingStaleCronJob.name)
|
||||
async handle(): Promise<void> {
|
||||
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<MessagingOngoingStaleJobData>(
|
||||
MessagingOngoingStaleJob.name,
|
||||
{
|
||||
workspaceId,
|
||||
workspaceId: activeWorkspace.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -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<Workspace>,
|
||||
@InjectRepository(DataSourceEntity, 'metadata')
|
||||
private readonly dataSourceRepository: Repository<DataSourceEntity>,
|
||||
@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,
|
||||
|
||||
Reference in New Issue
Block a user