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 { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import React from 'react';
|
||||||
import { IconComponent } from 'twenty-ui';
|
import { IconComponent } from 'twenty-ui';
|
||||||
|
|
||||||
export type MainButtonVariant = 'primary' | 'secondary';
|
export type MainButtonVariant = 'primary' | 'secondary';
|
||||||
@ -79,7 +79,7 @@ const StyledButton = styled.button<
|
|||||||
padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(3)};
|
padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(3)};
|
||||||
width: ${({ fullWidth, width }) =>
|
width: ${({ fullWidth, width }) =>
|
||||||
fullWidth ? '100%' : width ? `${width}px` : 'auto'};
|
fullWidth ? '100%' : width ? `${width}px` : 'auto'};
|
||||||
${({ theme, variant }) => {
|
${({ theme, variant, disabled }) => {
|
||||||
switch (variant) {
|
switch (variant) {
|
||||||
case 'secondary':
|
case 'secondary':
|
||||||
return `
|
return `
|
||||||
@ -90,7 +90,11 @@ const StyledButton = styled.button<
|
|||||||
default:
|
default:
|
||||||
return `
|
return `
|
||||||
&:hover {
|
&: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 { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
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 {
|
import {
|
||||||
Workspace,
|
Workspace,
|
||||||
WorkspaceActivationStatus,
|
WorkspaceActivationStatus,
|
||||||
@ -32,7 +32,7 @@ export class SetWorkspaceActivationStatusCommand extends CommandRunner {
|
|||||||
private readonly typeORMService: TypeORMService,
|
private readonly typeORMService: TypeORMService,
|
||||||
private readonly dataSourceService: DataSourceService,
|
private readonly dataSourceService: DataSourceService,
|
||||||
private readonly workspaceCacheVersionService: WorkspaceCacheVersionService,
|
private readonly workspaceCacheVersionService: WorkspaceCacheVersionService,
|
||||||
private readonly billingService: BillingService,
|
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
@ -56,7 +56,7 @@ export class SetWorkspaceActivationStatusCommand extends CommandRunner {
|
|||||||
activeSubscriptionWorkspaceIds = [options.workspaceId];
|
activeSubscriptionWorkspaceIds = [options.workspaceId];
|
||||||
} else {
|
} else {
|
||||||
activeSubscriptionWorkspaceIds =
|
activeSubscriptionWorkspaceIds =
|
||||||
await this.billingService.getActiveSubscriptionWorkspaceIds();
|
await this.billingSubscriptionService.getActiveSubscriptionWorkspaceIds();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!activeSubscriptionWorkspaceIds.length) {
|
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 { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
||||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
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 { 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';
|
import { AuthResolver } from './auth.resolver';
|
||||||
|
|
||||||
|
|||||||
@ -9,14 +9,14 @@ import {
|
|||||||
|
|
||||||
import { Response } from 'express';
|
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 { 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 { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
|
||||||
import { TokenService } from 'src/engine/core-modules/auth/services/token.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 { 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')
|
@Controller('auth/google-apis')
|
||||||
export class GoogleAPIsAuthController {
|
export class GoogleAPIsAuthController {
|
||||||
@ -93,10 +93,11 @@ export class GoogleAPIsAuthController {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
await onboardingServiceInstance.skipSyncEmailOnboardingStep(
|
await onboardingServiceInstance.toggleOnboardingConnectAccountCompletion({
|
||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
);
|
value: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.redirect(
|
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 { 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 { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||||
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/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', () => {
|
describe('SignInUpService', () => {
|
||||||
let service: SignInUpService;
|
let service: SignInUpService;
|
||||||
@ -21,10 +22,6 @@ describe('SignInUpService', () => {
|
|||||||
provide: FileUploadService,
|
provide: FileUploadService,
|
||||||
useValue: {},
|
useValue: {},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
provide: UserWorkspaceService,
|
|
||||||
useValue: {},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
provide: getRepositoryToken(Workspace, 'core'),
|
provide: getRepositoryToken(Workspace, 'core'),
|
||||||
useValue: {},
|
useValue: {},
|
||||||
@ -34,7 +31,11 @@ describe('SignInUpService', () => {
|
|||||||
useValue: {},
|
useValue: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: EnvironmentService,
|
provide: UserWorkspaceService,
|
||||||
|
useValue: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: OnboardingService,
|
||||||
useValue: {},
|
useValue: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -42,7 +43,7 @@ describe('SignInUpService', () => {
|
|||||||
useValue: {},
|
useValue: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: WorkspaceService,
|
provide: EnvironmentService,
|
||||||
useValue: {},
|
useValue: {},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@ -18,9 +18,9 @@ import {
|
|||||||
hashPassword,
|
hashPassword,
|
||||||
} from 'src/engine/core-modules/auth/auth.util';
|
} from 'src/engine/core-modules/auth/auth.util';
|
||||||
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
|
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 { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
|
|
||||||
import {
|
import {
|
||||||
Workspace,
|
Workspace,
|
||||||
WorkspaceActivationStatus,
|
WorkspaceActivationStatus,
|
||||||
@ -40,6 +40,7 @@ export type SignInUpServiceInput = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
|
||||||
export class SignInUpService {
|
export class SignInUpService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly fileUploadService: FileUploadService,
|
private readonly fileUploadService: FileUploadService,
|
||||||
@ -48,7 +49,7 @@ export class SignInUpService {
|
|||||||
@InjectRepository(User, 'core')
|
@InjectRepository(User, 'core')
|
||||||
private readonly userRepository: Repository<User>,
|
private readonly userRepository: Repository<User>,
|
||||||
private readonly userWorkspaceService: UserWorkspaceService,
|
private readonly userWorkspaceService: UserWorkspaceService,
|
||||||
private readonly workspaceService: WorkspaceService,
|
private readonly onboardingService: OnboardingService,
|
||||||
private readonly httpService: HttpService,
|
private readonly httpService: HttpService,
|
||||||
private readonly environmentService: EnvironmentService,
|
private readonly environmentService: EnvironmentService,
|
||||||
) {}
|
) {}
|
||||||
@ -221,6 +222,14 @@ export class SignInUpService {
|
|||||||
|
|
||||||
await this.userWorkspaceService.create(user.id, workspace.id);
|
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;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +1,18 @@
|
|||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
Headers,
|
Headers,
|
||||||
Req,
|
|
||||||
RawBodyRequest,
|
|
||||||
Logger,
|
Logger,
|
||||||
Post,
|
Post,
|
||||||
|
RawBodyRequest,
|
||||||
|
Req,
|
||||||
Res,
|
Res,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
|
|
||||||
import {
|
import { WebhookEvent } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service';
|
||||||
BillingWorkspaceService,
|
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||||
WebhookEvent,
|
import { BillingWebhookService } from 'src/engine/core-modules/billing/services/billing-webhook.service';
|
||||||
} from 'src/engine/core-modules/billing/billing.workspace-service';
|
|
||||||
import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service';
|
import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service';
|
||||||
|
|
||||||
@Controller('billing')
|
@Controller('billing')
|
||||||
@ -22,7 +21,8 @@ export class BillingController {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly stripeService: StripeService,
|
private readonly stripeService: StripeService,
|
||||||
private readonly billingWorkspaceService: BillingWorkspaceService,
|
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||||
|
private readonly billingWehbookService: BillingWebhookService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post('/webhooks')
|
@Post('/webhooks')
|
||||||
@ -42,7 +42,7 @@ export class BillingController {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (event.type === WebhookEvent.SETUP_INTENT_SUCCEEDED) {
|
if (event.type === WebhookEvent.SETUP_INTENT_SUCCEEDED) {
|
||||||
await this.billingWorkspaceService.handleUnpaidInvoices(event.data);
|
await this.billingSubscriptionService.handleUnpaidInvoices(event.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -58,7 +58,7 @@ export class BillingController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.billingWorkspaceService.upsertBillingSubscription(
|
await this.billingWehbookService.processStripeEvent(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
event.data,
|
event.data,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,16 +2,17 @@ import { Module } from '@nestjs/common';
|
|||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { BillingController } from 'src/engine/core-modules/billing/billing.controller';
|
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 { 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 { 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 { 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({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@ -29,11 +30,16 @@ import { BillingWorkspaceService } from 'src/engine/core-modules/billing/billing
|
|||||||
],
|
],
|
||||||
controllers: [BillingController],
|
controllers: [BillingController],
|
||||||
providers: [
|
providers: [
|
||||||
BillingService,
|
BillingSubscriptionService,
|
||||||
BillingWorkspaceService,
|
BillingWebhookService,
|
||||||
|
BillingPortalWorkspaceService,
|
||||||
BillingResolver,
|
BillingResolver,
|
||||||
BillingWorkspaceMemberListener,
|
BillingWorkspaceMemberListener,
|
||||||
],
|
],
|
||||||
exports: [BillingService, BillingWorkspaceService],
|
exports: [
|
||||||
|
BillingSubscriptionService,
|
||||||
|
BillingPortalWorkspaceService,
|
||||||
|
BillingWebhookService,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class BillingModule {}
|
export class BillingModule {}
|
||||||
|
|||||||
@ -1,43 +1,34 @@
|
|||||||
import { UseGuards } from '@nestjs/common';
|
import { UseGuards } from '@nestjs/common';
|
||||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||||
|
|
||||||
import {
|
import { AvailableProduct } from 'src/engine/core-modules/billing/interfaces/available-product.interface';
|
||||||
AvailableProduct,
|
|
||||||
BillingWorkspaceService,
|
|
||||||
} from 'src/engine/core-modules/billing/billing.workspace-service';
|
|
||||||
import { BillingSessionInput } from 'src/engine/core-modules/billing/dto/billing-session.input';
|
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 { CheckoutSessionInput } from 'src/engine/core-modules/billing/dto/checkout-session.input';
|
||||||
import { ProductPricesEntity } from 'src/engine/core-modules/billing/dto/product-prices.entity';
|
import { ProductPricesEntity } from 'src/engine/core-modules/billing/dto/product-prices.entity';
|
||||||
import { ProductInput } from 'src/engine/core-modules/billing/dto/product.input';
|
import { ProductInput } from 'src/engine/core-modules/billing/dto/product.input';
|
||||||
import { SessionEntity } from 'src/engine/core-modules/billing/dto/session.entity';
|
import { SessionEntity } from 'src/engine/core-modules/billing/dto/session.entity';
|
||||||
import { UpdateBillingEntity } from 'src/engine/core-modules/billing/dto/update-billing.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 { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||||
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
||||||
import { assert } from 'src/utils/assert';
|
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
export class BillingResolver {
|
export class BillingResolver {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly billingWorkspaceService: BillingWorkspaceService,
|
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||||
|
private readonly billingPortalWorkspaceService: BillingPortalWorkspaceService,
|
||||||
|
private readonly stripeService: StripeService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Query(() => ProductPricesEntity)
|
@Query(() => ProductPricesEntity)
|
||||||
async getProductPrices(@Args() { product }: ProductInput) {
|
async getProductPrices(@Args() { product }: ProductInput) {
|
||||||
const stripeProductId =
|
const productPrices = await this.stripeService.getStripePrices(product);
|
||||||
this.billingWorkspaceService.getProductStripeId(product);
|
|
||||||
|
|
||||||
assert(
|
|
||||||
stripeProductId,
|
|
||||||
`Product '${product}' not found, available products are ['${Object.values(
|
|
||||||
AvailableProduct,
|
|
||||||
).join("','")}']`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const productPrices =
|
|
||||||
await this.billingWorkspaceService.getProductPrices(stripeProductId);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalNumberOfPrices: productPrices.length,
|
totalNumberOfPrices: productPrices.length,
|
||||||
@ -52,7 +43,7 @@ export class BillingResolver {
|
|||||||
@Args() { returnUrlPath }: BillingSessionInput,
|
@Args() { returnUrlPath }: BillingSessionInput,
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
url: await this.billingWorkspaceService.computeBillingPortalSessionURL(
|
url: await this.billingPortalWorkspaceService.computeBillingPortalSessionURL(
|
||||||
user.defaultWorkspaceId,
|
user.defaultWorkspaceId,
|
||||||
returnUrlPath,
|
returnUrlPath,
|
||||||
),
|
),
|
||||||
@ -66,32 +57,22 @@ export class BillingResolver {
|
|||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
@Args() { recurringInterval, successUrlPath }: CheckoutSessionInput,
|
@Args() { recurringInterval, successUrlPath }: CheckoutSessionInput,
|
||||||
) {
|
) {
|
||||||
const stripeProductId = this.billingWorkspaceService.getProductStripeId(
|
const productPrice = await this.stripeService.getStripePrice(
|
||||||
AvailableProduct.BasePlan,
|
AvailableProduct.BasePlan,
|
||||||
|
recurringInterval,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(
|
if (!productPrice) {
|
||||||
stripeProductId,
|
throw new Error(
|
||||||
'BasePlan productId not found, please check your BILLING_STRIPE_BASE_PLAN_PRODUCT_ID env variable',
|
'Product price not found for the given recurring interval',
|
||||||
);
|
);
|
||||||
|
}
|
||||||
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`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
url: await this.billingWorkspaceService.computeCheckoutSessionURL(
|
url: await this.billingPortalWorkspaceService.computeCheckoutSessionURL(
|
||||||
user,
|
user,
|
||||||
workspace,
|
workspace,
|
||||||
stripePriceId,
|
productPrice.stripePriceId,
|
||||||
successUrlPath,
|
successUrlPath,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
@ -100,7 +81,7 @@ export class BillingResolver {
|
|||||||
@Mutation(() => UpdateBillingEntity)
|
@Mutation(() => UpdateBillingEntity)
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
async updateBillingSubscription(@AuthUser() user: User) {
|
async updateBillingSubscription(@AuthUser() user: User) {
|
||||||
await this.billingWorkspaceService.updateBillingSubscription(user);
|
await this.billingSubscriptionService.applyBillingSubscription(user);
|
||||||
|
|
||||||
return { success: true };
|
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 { 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()
|
@ArgsType()
|
||||||
export class ProductInput {
|
export class ProductInput {
|
||||||
|
|||||||
@ -0,0 +1,3 @@
|
|||||||
|
export enum AvailableProduct {
|
||||||
|
BasePlan = 'base-plan',
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { Logger, Scope } from '@nestjs/common';
|
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 { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service';
|
||||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||||
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
|
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);
|
protected readonly logger = new Logger(UpdateSubscriptionJob.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly billingWorkspaceService: BillingWorkspaceService,
|
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||||
private readonly userWorkspaceService: UserWorkspaceService,
|
private readonly userWorkspaceService: UserWorkspaceService,
|
||||||
private readonly stripeService: StripeService,
|
private readonly stripeService: StripeService,
|
||||||
) {}
|
) {}
|
||||||
@ -33,7 +33,7 @@ export class UpdateSubscriptionJob {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const billingSubscriptionItem =
|
const billingSubscriptionItem =
|
||||||
await this.billingWorkspaceService.getBillingSubscriptionItem(
|
await this.billingSubscriptionService.getCurrentBillingSubscriptionItem(
|
||||||
data.workspaceId,
|
data.workspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -1,23 +1,23 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { OnEvent } from '@nestjs/event-emitter';
|
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 {
|
import {
|
||||||
UpdateSubscriptionJob,
|
UpdateSubscriptionJob,
|
||||||
UpdateSubscriptionJobData,
|
UpdateSubscriptionJobData,
|
||||||
} from 'src/engine/core-modules/billing/jobs/update-subscription.job';
|
} 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 { 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()
|
@Injectable()
|
||||||
export class BillingWorkspaceMemberListener {
|
export class BillingWorkspaceMemberListener {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectMessageQueue(MessageQueue.billingQueue)
|
@InjectMessageQueue(MessageQueue.billingQueue)
|
||||||
private readonly messageQueueService: MessageQueueService,
|
private readonly messageQueueService: MessageQueueService,
|
||||||
private readonly billingWorkspaceService: BillingWorkspaceService,
|
private readonly environmentService: EnvironmentService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@OnEvent('workspaceMember.created')
|
@OnEvent('workspaceMember.created')
|
||||||
@ -25,12 +25,7 @@ export class BillingWorkspaceMemberListener {
|
|||||||
async handleCreateOrDeleteEvent(
|
async handleCreateOrDeleteEvent(
|
||||||
payload: ObjectRecordCreateEvent<WorkspaceMemberWorkspaceEntity>,
|
payload: ObjectRecordCreateEvent<WorkspaceMemberWorkspaceEntity>,
|
||||||
) {
|
) {
|
||||||
const isBillingEnabledForWorkspace =
|
if (!this.environmentService.get('IS_BILLING_ENABLED')) {
|
||||||
await this.billingWorkspaceService.isBillingEnabledForWorkspace(
|
|
||||||
payload.workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isBillingEnabledForWorkspace) {
|
|
||||||
return;
|
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 Stripe from 'stripe';
|
||||||
|
|
||||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
import { AvailableProduct } from 'src/engine/core-modules/billing/interfaces/available-product.interface';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
|
||||||
|
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 { 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()
|
@Injectable()
|
||||||
export class StripeService {
|
export class StripeService {
|
||||||
@ -30,10 +33,28 @@ export class StripeService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProductPrices(stripeProductId: string) {
|
async getStripePrices(product: AvailableProduct) {
|
||||||
return this.stripe.prices.search({
|
const stripeProductId = this.getStripeProductId(product);
|
||||||
|
|
||||||
|
const prices = await this.stripe.prices.search({
|
||||||
query: `product: '${stripeProductId}'`,
|
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) {
|
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 { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
||||||
import { OnboardingResolver } from 'src/engine/core-modules/onboarding/onboarding.resolver';
|
import { OnboardingResolver } from 'src/engine/core-modules/onboarding/onboarding.resolver';
|
||||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
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 { 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({
|
@Module({
|
||||||
imports: [
|
imports: [BillingModule, UserVarsModule],
|
||||||
DataSourceModule,
|
|
||||||
WorkspaceManagerModule,
|
|
||||||
UserWorkspaceModule,
|
|
||||||
EnvironmentModule,
|
|
||||||
BillingModule,
|
|
||||||
UserVarsModule,
|
|
||||||
],
|
|
||||||
exports: [OnboardingService],
|
exports: [OnboardingService],
|
||||||
providers: [OnboardingService, OnboardingResolver],
|
providers: [OnboardingService, OnboardingResolver],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import { UseGuards } from '@nestjs/common';
|
import { UseGuards } from '@nestjs/common';
|
||||||
import { Mutation, Resolver } from '@nestjs/graphql';
|
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 { 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 { 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)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Resolver()
|
@Resolver()
|
||||||
@ -19,10 +19,11 @@ export class OnboardingResolver {
|
|||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
@AuthWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
): Promise<OnboardingStepSuccess> {
|
): Promise<OnboardingStepSuccess> {
|
||||||
await this.onboardingService.skipSyncEmailOnboardingStep(
|
await this.onboardingService.toggleOnboardingConnectAccountCompletion({
|
||||||
user.id,
|
userId: user.id,
|
||||||
workspace.id,
|
workspaceId: workspace.id,
|
||||||
);
|
value: true,
|
||||||
|
});
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,59 +1,43 @@
|
|||||||
/* eslint-disable @nx/workspace-inject-workspace-repository */
|
|
||||||
import { Injectable } from '@nestjs/common';
|
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 { 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 { 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 { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { WorkspaceActivationStatus } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||||
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 { isDefined } from 'src/utils/is-defined';
|
import { isDefined } from 'src/utils/is-defined';
|
||||||
|
|
||||||
enum OnboardingStepValues {
|
export enum OnboardingStepKeys {
|
||||||
SKIPPED = 'SKIPPED',
|
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 {
|
export type OnboardingKeyValueTypeMap = {
|
||||||
SYNC_EMAIL_ONBOARDING_STEP = 'SYNC_EMAIL_ONBOARDING_STEP',
|
[OnboardingStepKeys.ONBOARDING_CONNECT_ACCOUNT_COMPLETE]: boolean;
|
||||||
INVITE_TEAM_ONBOARDING_STEP = 'INVITE_TEAM_ONBOARDING_STEP',
|
[OnboardingStepKeys.ONBOARDING_INVITE_TEAM_COMPLETE]: boolean;
|
||||||
}
|
[OnboardingStepKeys.ONBOARDING_CREATE_PROFILE_COMPLETE]: boolean;
|
||||||
|
|
||||||
type OnboardingKeyValueTypeMap = {
|
|
||||||
[OnboardingStepKeys.SYNC_EMAIL_ONBOARDING_STEP]: OnboardingStepValues;
|
|
||||||
[OnboardingStepKeys.INVITE_TEAM_ONBOARDING_STEP]: OnboardingStepValues;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OnboardingService {
|
export class OnboardingService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly twentyORMManager: TwentyORMManager,
|
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||||
private readonly billingWorkspaceService: BillingWorkspaceService,
|
private readonly environmentService: EnvironmentService,
|
||||||
private readonly workspaceManagerService: WorkspaceManagerService,
|
|
||||||
private readonly userWorkspaceService: UserWorkspaceService,
|
|
||||||
private readonly userVarsService: UserVarsService<OnboardingKeyValueTypeMap>,
|
private readonly userVarsService: UserVarsService<OnboardingKeyValueTypeMap>,
|
||||||
@InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity)
|
|
||||||
private readonly connectedAccountRepository: ConnectedAccountRepository,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private async isSubscriptionIncompleteOnboardingStatus(user: User) {
|
private async isSubscriptionIncompleteOnboardingStatus(user: User) {
|
||||||
const isBillingEnabledForWorkspace =
|
const isBillingEnabled = this.environmentService.get('IS_BILLING_ENABLED');
|
||||||
await this.billingWorkspaceService.isBillingEnabledForWorkspace(
|
|
||||||
user.defaultWorkspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isBillingEnabledForWorkspace) {
|
if (!isBillingEnabled) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentBillingSubscription =
|
const currentBillingSubscription =
|
||||||
await this.billingWorkspaceService.getCurrentBillingSubscription({
|
await this.billingSubscriptionService.getCurrentBillingSubscription({
|
||||||
workspaceId: user.defaultWorkspaceId,
|
workspaceId: user.defaultWorkspaceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -64,57 +48,9 @@ export class OnboardingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async isWorkspaceActivationOnboardingStatus(user: User) {
|
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 (
|
return (
|
||||||
workspaceMember &&
|
user.defaultWorkspace.activationStatus ===
|
||||||
(!workspaceMember.name.firstName || !workspaceMember.name.lastName)
|
WorkspaceActivationStatus.PENDING_CREATION
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,35 +63,82 @@ export class OnboardingService {
|
|||||||
return OnboardingStatus.WORKSPACE_ACTIVATION;
|
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;
|
return OnboardingStatus.PROFILE_CREATION;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await this.isSyncEmailOnboardingStatus(user)) {
|
if (!isConnectAccountComplete) {
|
||||||
return OnboardingStatus.SYNC_EMAIL;
|
return OnboardingStatus.SYNC_EMAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await this.isInviteTeamOnboardingStatus(user.defaultWorkspace)) {
|
if (!isInviteTeamComplete) {
|
||||||
return OnboardingStatus.INVITE_TEAM;
|
return OnboardingStatus.INVITE_TEAM;
|
||||||
}
|
}
|
||||||
|
|
||||||
return OnboardingStatus.COMPLETED;
|
return OnboardingStatus.COMPLETED;
|
||||||
}
|
}
|
||||||
|
|
||||||
async skipInviteTeamOnboardingStep(workspaceId: string) {
|
async toggleOnboardingConnectAccountCompletion({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
value: boolean;
|
||||||
|
}) {
|
||||||
await this.userVarsService.set({
|
await this.userVarsService.set({
|
||||||
workspaceId,
|
userId,
|
||||||
key: OnboardingStepKeys.INVITE_TEAM_ONBOARDING_STEP,
|
workspaceId: workspaceId,
|
||||||
value: OnboardingStepValues.SKIPPED,
|
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({
|
await this.userVarsService.set({
|
||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
key: OnboardingStepKeys.SYNC_EMAIL_ONBOARDING_STEP,
|
key: OnboardingStepKeys.ONBOARDING_CREATE_PROFILE_COMPLETE,
|
||||||
value: OnboardingStepValues.SKIPPED,
|
value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -37,6 +37,7 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
|
|||||||
|
|
||||||
payload.workspaceId = workspaceId;
|
payload.workspaceId = workspaceId;
|
||||||
payload.userId = userId;
|
payload.userId = userId;
|
||||||
|
payload.name = 'user.signup';
|
||||||
|
|
||||||
this.eventEmitter.emit('user.signup', payload);
|
this.eventEmitter.emit('user.signup', payload);
|
||||||
|
|
||||||
@ -80,6 +81,7 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
|
|||||||
after: workspaceMember[0],
|
after: workspaceMember[0],
|
||||||
};
|
};
|
||||||
payload.recordId = workspaceMember[0].id;
|
payload.recordId = workspaceMember[0].id;
|
||||||
|
payload.name = 'workspaceMember.created';
|
||||||
|
|
||||||
this.eventEmitter.emit('workspaceMember.created', payload);
|
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 { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
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> {
|
export class UserService extends TypeOrmQueryService<User> {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(User, 'core')
|
@InjectRepository(User, 'core')
|
||||||
@ -115,6 +116,7 @@ export class UserService extends TypeOrmQueryService<User> {
|
|||||||
};
|
};
|
||||||
payload.name = 'workspaceMember.deleted';
|
payload.name = 'workspaceMember.deleted';
|
||||||
payload.recordId = workspaceMember.id;
|
payload.recordId = workspaceMember.id;
|
||||||
|
payload.name = 'workspaceMember.deleted';
|
||||||
|
|
||||||
this.eventEmitter.emit('workspaceMember.deleted', payload);
|
this.eventEmitter.emit('workspaceMember.deleted', payload);
|
||||||
|
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||||
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
|
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
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 { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||||
import { UserService } from 'src/engine/core-modules/user/services/user.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 { EmailService } from 'src/engine/integrations/email/email.service';
|
||||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.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';
|
import { WorkspaceService } from './workspace.service';
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ describe('WorkspaceService', () => {
|
|||||||
useValue: {},
|
useValue: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: BillingWorkspaceService,
|
provide: BillingSubscriptionService,
|
||||||
useValue: {},
|
useValue: {},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { render } from '@react-email/render';
|
|||||||
import { SendInviteLinkEmail } from 'twenty-emails';
|
import { SendInviteLinkEmail } from 'twenty-emails';
|
||||||
import { Repository } from 'typeorm';
|
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 { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
import { 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 { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||||
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.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> {
|
export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Workspace, 'core')
|
@InjectRepository(Workspace, 'core')
|
||||||
@ -33,7 +34,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
|||||||
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
|
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
|
||||||
private readonly workspaceManagerService: WorkspaceManagerService,
|
private readonly workspaceManagerService: WorkspaceManagerService,
|
||||||
private readonly userWorkspaceService: UserWorkspaceService,
|
private readonly userWorkspaceService: UserWorkspaceService,
|
||||||
private readonly billingWorkspaceService: BillingWorkspaceService,
|
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||||
private readonly environmentService: EnvironmentService,
|
private readonly environmentService: EnvironmentService,
|
||||||
private readonly emailService: EmailService,
|
private readonly emailService: EmailService,
|
||||||
private readonly onboardingService: OnboardingService,
|
private readonly onboardingService: OnboardingService,
|
||||||
@ -65,7 +66,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
|||||||
assert(workspace, 'Workspace not found');
|
assert(workspace, 'Workspace not found');
|
||||||
|
|
||||||
await this.userWorkspaceRepository.delete({ workspaceId: id });
|
await this.userWorkspaceRepository.delete({ workspaceId: id });
|
||||||
await this.billingWorkspaceService.deleteSubscription(workspace.id);
|
await this.billingSubscriptionService.deleteSubscription(workspace.id);
|
||||||
|
|
||||||
await this.workspaceManagerService.delete(id);
|
await this.workspaceManagerService.delete(id);
|
||||||
|
|
||||||
@ -99,7 +100,6 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
|
|||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
});
|
});
|
||||||
await this.onboardingService.skipInviteTeamOnboardingStep(workspaceId);
|
|
||||||
await this.reassignOrRemoveUserDefaultWorkspace(workspaceId, userId);
|
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 };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +1,51 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { OnEvent } from '@nestjs/event-emitter';
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||||
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 {
|
import {
|
||||||
HandleWorkspaceMemberDeletedJob,
|
HandleWorkspaceMemberDeletedJob,
|
||||||
HandleWorkspaceMemberDeletedJobData,
|
HandleWorkspaceMemberDeletedJobData,
|
||||||
} from 'src/engine/core-modules/workspace/handle-workspace-member-deleted.job';
|
} 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 { 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()
|
@Injectable()
|
||||||
export class WorkspaceWorkspaceMemberListener {
|
export class WorkspaceWorkspaceMemberListener {
|
||||||
constructor(
|
constructor(
|
||||||
|
private readonly onboardingService: OnboardingService,
|
||||||
@InjectMessageQueue(MessageQueue.workspaceQueue)
|
@InjectMessageQueue(MessageQueue.workspaceQueue)
|
||||||
private readonly messageQueueService: MessageQueueService,
|
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')
|
@OnEvent('workspaceMember.deleted')
|
||||||
async handleDeleteEvent(
|
async handleDeleteEvent(
|
||||||
payload: ObjectRecordDeleteEvent<WorkspaceMemberWorkspaceEntity>,
|
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 { 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 { WorkspaceCacheVersionModule } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.module';
|
||||||
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.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 { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts';
|
||||||
import { Workspace } from './workspace.entity';
|
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 { 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 { 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 { 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 { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
@ -41,7 +41,7 @@ export class WorkspaceResolver {
|
|||||||
private readonly workspaceCacheVersionService: WorkspaceCacheVersionService,
|
private readonly workspaceCacheVersionService: WorkspaceCacheVersionService,
|
||||||
private readonly userWorkspaceService: UserWorkspaceService,
|
private readonly userWorkspaceService: UserWorkspaceService,
|
||||||
private readonly fileUploadService: FileUploadService,
|
private readonly fileUploadService: FileUploadService,
|
||||||
private readonly billingWorkspaceService: BillingWorkspaceService,
|
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Query(() => Workspace)
|
@Query(() => Workspace)
|
||||||
@ -112,7 +112,7 @@ export class WorkspaceResolver {
|
|||||||
async currentBillingSubscription(
|
async currentBillingSubscription(
|
||||||
@Parent() workspace: Workspace,
|
@Parent() workspace: Workspace,
|
||||||
): Promise<BillingSubscription | null> {
|
): Promise<BillingSubscription | null> {
|
||||||
return this.billingWorkspaceService.getCurrentBillingSubscription({
|
return this.billingSubscriptionService.getCurrentBillingSubscription({
|
||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,10 +15,10 @@ export const companyPrefillDemoData = async (
|
|||||||
'addressAddressCity',
|
'addressAddressCity',
|
||||||
'employees',
|
'employees',
|
||||||
'linkedinLinkPrimaryLinkUrl',
|
'linkedinLinkPrimaryLinkUrl',
|
||||||
'position',
|
|
||||||
'createdBySource',
|
'createdBySource',
|
||||||
'createdByWorkspaceMemberId',
|
'createdByWorkspaceMemberId',
|
||||||
'createdByName'
|
'createdByName',
|
||||||
|
'position'
|
||||||
])
|
])
|
||||||
.orIgnore()
|
.orIgnore()
|
||||||
.values(
|
.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 { EntityManager } from 'typeorm';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
@ -25,6 +26,9 @@ const generateOpportunities = (companies) => {
|
|||||||
stage: getRandomStage(),
|
stage: getRandomStage(),
|
||||||
pointOfContactId: company.personId,
|
pointOfContactId: company.personId,
|
||||||
companyId: company.id,
|
companyId: company.id,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEMO_SEED_WORKSPACE_MEMBER_IDS.NOAH,
|
||||||
|
createdByName: 'Noah A',
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -53,6 +57,9 @@ export const opportunityPrefillDemoData = async (
|
|||||||
'stage',
|
'stage',
|
||||||
'pointOfContactId',
|
'pointOfContactId',
|
||||||
'companyId',
|
'companyId',
|
||||||
|
'createdBySource',
|
||||||
|
'createdByWorkspaceMemberId',
|
||||||
|
'createdByName',
|
||||||
'position',
|
'position',
|
||||||
])
|
])
|
||||||
.orIgnore()
|
.orIgnore()
|
||||||
|
|||||||
@ -19,11 +19,11 @@ export const personPrefillDemoData = async (
|
|||||||
jobTitle: person.jobTitle,
|
jobTitle: person.jobTitle,
|
||||||
city: person.city,
|
city: person.city,
|
||||||
avatarUrl: person.avatarUrl,
|
avatarUrl: person.avatarUrl,
|
||||||
position: index,
|
|
||||||
companyId: companies[Math.floor(index / 2)].id,
|
companyId: companies[Math.floor(index / 2)].id,
|
||||||
createdBySource: person.createdBySource,
|
createdBySource: person.createdBySource,
|
||||||
createdByWorkspaceMemberId: person.createdByWorkspaceMemberId,
|
createdByWorkspaceMemberId: person.createdByWorkspaceMemberId,
|
||||||
createdByName: person.createdByName
|
createdByName: person.createdByName,
|
||||||
|
position: index
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await entityManager
|
await entityManager
|
||||||
@ -37,11 +37,11 @@ export const personPrefillDemoData = async (
|
|||||||
'jobTitle',
|
'jobTitle',
|
||||||
'city',
|
'city',
|
||||||
'avatarUrl',
|
'avatarUrl',
|
||||||
'position',
|
|
||||||
'companyId',
|
'companyId',
|
||||||
'createdBySource',
|
'createdBySource',
|
||||||
'createdByWorkspaceMemberId',
|
'createdByWorkspaceMemberId',
|
||||||
'createdByName',
|
'createdByName',
|
||||||
|
'position',
|
||||||
])
|
])
|
||||||
.orIgnore()
|
.orIgnore()
|
||||||
.values(people)
|
.values(people)
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||||||
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
||||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
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 { 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 { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||||
@ -44,7 +45,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
|
|||||||
WorkspaceMemberWorkspaceEntity,
|
WorkspaceMemberWorkspaceEntity,
|
||||||
]),
|
]),
|
||||||
CalendarEventParticipantManagerModule,
|
CalendarEventParticipantManagerModule,
|
||||||
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
|
TypeOrmModule.forFeature([FeatureFlagEntity, Workspace], 'core'),
|
||||||
TypeOrmModule.forFeature([DataSourceEntity], 'metadata'),
|
TypeOrmModule.forFeature([DataSourceEntity], 'metadata'),
|
||||||
WorkspaceDataSourceModule,
|
WorkspaceDataSourceModule,
|
||||||
CalendarEventCleanerModule,
|
CalendarEventCleanerModule,
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
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 { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator';
|
||||||
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
|
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
|
||||||
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
|
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
|
||||||
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
||||||
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
|
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 { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
import {
|
import {
|
||||||
CalendarEventListFetchJob,
|
CalendarEventListFetchJob,
|
||||||
@ -21,33 +23,25 @@ import { CalendarChannelSyncStage } from 'src/modules/calendar/common/standard-o
|
|||||||
})
|
})
|
||||||
export class CalendarEventListFetchCronJob {
|
export class CalendarEventListFetchCronJob {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(DataSourceEntity, 'metadata')
|
@InjectRepository(Workspace, 'core')
|
||||||
private readonly dataSourceRepository: Repository<DataSourceEntity>,
|
private readonly workspaceRepository: Repository<Workspace>,
|
||||||
@InjectMessageQueue(MessageQueue.calendarQueue)
|
@InjectMessageQueue(MessageQueue.calendarQueue)
|
||||||
private readonly messageQueueService: MessageQueueService,
|
private readonly messageQueueService: MessageQueueService,
|
||||||
private readonly billingService: BillingService,
|
|
||||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Process(CalendarEventListFetchCronJob.name)
|
@Process(CalendarEventListFetchCronJob.name)
|
||||||
async handle(): Promise<void> {
|
async handle(): Promise<void> {
|
||||||
const workspaceIds =
|
const activeWorkspaces = await this.workspaceRepository.find({
|
||||||
await this.billingService.getActiveSubscriptionWorkspaceIds();
|
|
||||||
|
|
||||||
const dataSources = await this.dataSourceRepository.find({
|
|
||||||
where: {
|
where: {
|
||||||
workspaceId: In(workspaceIds),
|
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const workspaceIdsWithDataSources = new Set(
|
for (const activeWorkspace of activeWorkspaces) {
|
||||||
dataSources.map((dataSource) => dataSource.workspaceId),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const workspaceId of workspaceIdsWithDataSources) {
|
|
||||||
const calendarChannelRepository =
|
const calendarChannelRepository =
|
||||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||||
workspaceId,
|
activeWorkspace.id,
|
||||||
'calendarChannel',
|
'calendarChannel',
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -66,7 +60,7 @@ export class CalendarEventListFetchCronJob {
|
|||||||
CalendarEventListFetchJob.name,
|
CalendarEventListFetchJob.name,
|
||||||
{
|
{
|
||||||
calendarChannelId: calendarChannel.id,
|
calendarChannelId: calendarChannel.id,
|
||||||
workspaceId,
|
workspaceId: activeWorkspace.id,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -141,6 +141,7 @@ export class CalendarSaveEventsService {
|
|||||||
|
|
||||||
this.eventEmitter.emit(`calendarEventParticipant.matched`, {
|
this.eventEmitter.emit(`calendarEventParticipant.matched`, {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
name: 'calendarEventParticipant.matched',
|
||||||
workspaceMemberId: connectedAccount.accountOwnerId,
|
workspaceMemberId: connectedAccount.accountOwnerId,
|
||||||
calendarEventParticipants: savedCalendarEventParticipantsToEmit,
|
calendarEventParticipants: savedCalendarEventParticipantsToEmit,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -157,6 +157,7 @@ export class MatchParticipantService<
|
|||||||
|
|
||||||
this.eventEmitter.emit(`${objectMetadataName}.matched`, {
|
this.eventEmitter.emit(`${objectMetadataName}.matched`, {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
name: `${objectMetadataName}.matched`,
|
||||||
workspaceMemberId: null,
|
workspaceMemberId: null,
|
||||||
participants: updatedParticipants,
|
participants: updatedParticipants,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,9 +1,15 @@
|
|||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
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 { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
||||||
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
|
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
|
||||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||||
@ -13,46 +19,35 @@ import {
|
|||||||
MessageChannelWorkspaceEntity,
|
MessageChannelWorkspaceEntity,
|
||||||
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||||
import {
|
import {
|
||||||
MessagingMessageListFetchJobData,
|
|
||||||
MessagingMessageListFetchJob,
|
MessagingMessageListFetchJob,
|
||||||
|
MessagingMessageListFetchJobData,
|
||||||
} from 'src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job';
|
} 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)
|
@Processor(MessageQueue.cronQueue)
|
||||||
export class MessagingMessageListFetchCronJob {
|
export class MessagingMessageListFetchCronJob {
|
||||||
private readonly logger = new Logger(MessagingMessageListFetchCronJob.name);
|
private readonly logger = new Logger(MessagingMessageListFetchCronJob.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(DataSourceEntity, 'metadata')
|
@InjectRepository(Workspace, 'core')
|
||||||
private readonly dataSourceRepository: Repository<DataSourceEntity>,
|
private readonly workspaceRepository: Repository<Workspace>,
|
||||||
@InjectMessageQueue(MessageQueue.messagingQueue)
|
@InjectMessageQueue(MessageQueue.messagingQueue)
|
||||||
private readonly messageQueueService: MessageQueueService,
|
private readonly messageQueueService: MessageQueueService,
|
||||||
@InjectObjectMetadataRepository(MessageChannelWorkspaceEntity)
|
@InjectObjectMetadataRepository(MessageChannelWorkspaceEntity)
|
||||||
private readonly messageChannelRepository: MessageChannelRepository,
|
private readonly messageChannelRepository: MessageChannelRepository,
|
||||||
private readonly billingService: BillingService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Process(MessagingMessageListFetchCronJob.name)
|
@Process(MessagingMessageListFetchCronJob.name)
|
||||||
async handle(): Promise<void> {
|
async handle(): Promise<void> {
|
||||||
const workspaceIds =
|
const activeWorkspaces = await this.workspaceRepository.find({
|
||||||
await this.billingService.getActiveSubscriptionWorkspaceIds();
|
|
||||||
|
|
||||||
const dataSources = await this.dataSourceRepository.find({
|
|
||||||
where: {
|
where: {
|
||||||
workspaceId: In(workspaceIds),
|
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const workspaceIdsWithDataSources = new Set(
|
for (const activeWorkspace of activeWorkspaces) {
|
||||||
dataSources.map((dataSource) => dataSource.workspaceId),
|
const messageChannels = await this.messageChannelRepository.getAll(
|
||||||
);
|
activeWorkspace.id,
|
||||||
|
);
|
||||||
for (const workspaceId of workspaceIdsWithDataSources) {
|
|
||||||
const messageChannels =
|
|
||||||
await this.messageChannelRepository.getAll(workspaceId);
|
|
||||||
|
|
||||||
for (const messageChannel of messageChannels) {
|
for (const messageChannel of messageChannels) {
|
||||||
if (
|
if (
|
||||||
@ -65,7 +60,7 @@ export class MessagingMessageListFetchCronJob {
|
|||||||
await this.messageQueueService.add<MessagingMessageListFetchJobData>(
|
await this.messageQueueService.add<MessagingMessageListFetchJobData>(
|
||||||
MessagingMessageListFetchJob.name,
|
MessagingMessageListFetchJob.name,
|
||||||
{
|
{
|
||||||
workspaceId,
|
workspaceId: activeWorkspace.id,
|
||||||
messageChannelId: messageChannel.id,
|
messageChannelId: messageChannel.id,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,58 +1,53 @@
|
|||||||
import { Logger } from '@nestjs/common';
|
import { Logger } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
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 { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
||||||
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
|
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 { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||||
|
import { MessageChannelRepository } from 'src/modules/messaging/common/repositories/message-channel.repository';
|
||||||
import {
|
import {
|
||||||
MessageChannelSyncStage,
|
MessageChannelSyncStage,
|
||||||
MessageChannelWorkspaceEntity,
|
MessageChannelWorkspaceEntity,
|
||||||
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
} 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)
|
@Processor(MessageQueue.cronQueue)
|
||||||
export class MessagingMessagesImportCronJob {
|
export class MessagingMessagesImportCronJob {
|
||||||
private readonly logger = new Logger(MessagingMessagesImportCronJob.name);
|
private readonly logger = new Logger(MessagingMessagesImportCronJob.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(DataSourceEntity, 'metadata')
|
@InjectRepository(Workspace, 'core')
|
||||||
private readonly dataSourceRepository: Repository<DataSourceEntity>,
|
private readonly workspaceRepository: Repository<Workspace>,
|
||||||
@InjectMessageQueue(MessageQueue.messagingQueue)
|
@InjectMessageQueue(MessageQueue.messagingQueue)
|
||||||
private readonly messageQueueService: MessageQueueService,
|
private readonly messageQueueService: MessageQueueService,
|
||||||
@InjectObjectMetadataRepository(MessageChannelWorkspaceEntity)
|
@InjectObjectMetadataRepository(MessageChannelWorkspaceEntity)
|
||||||
private readonly messageChannelRepository: MessageChannelRepository,
|
private readonly messageChannelRepository: MessageChannelRepository,
|
||||||
private readonly billingService: BillingService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Process(MessagingMessagesImportCronJob.name)
|
@Process(MessagingMessagesImportCronJob.name)
|
||||||
async handle(): Promise<void> {
|
async handle(): Promise<void> {
|
||||||
const workspaceIds =
|
const activeWorkspaces = await this.workspaceRepository.find({
|
||||||
await this.billingService.getActiveSubscriptionWorkspaceIds();
|
|
||||||
|
|
||||||
const dataSources = await this.dataSourceRepository.find({
|
|
||||||
where: {
|
where: {
|
||||||
workspaceId: In(workspaceIds),
|
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const workspaceIdsWithDataSources = new Set(
|
for (const activeWorkspace of activeWorkspaces) {
|
||||||
dataSources.map((dataSource) => dataSource.workspaceId),
|
const messageChannels = await this.messageChannelRepository.getAll(
|
||||||
);
|
activeWorkspace.id,
|
||||||
|
);
|
||||||
for (const workspaceId of workspaceIdsWithDataSources) {
|
|
||||||
const messageChannels =
|
|
||||||
await this.messageChannelRepository.getAll(workspaceId);
|
|
||||||
|
|
||||||
for (const messageChannel of messageChannels) {
|
for (const messageChannel of messageChannels) {
|
||||||
if (
|
if (
|
||||||
@ -63,7 +58,7 @@ export class MessagingMessagesImportCronJob {
|
|||||||
await this.messageQueueService.add<MessagingMessagesImportJobData>(
|
await this.messageQueueService.add<MessagingMessagesImportJobData>(
|
||||||
MessagingMessagesImportJob.name,
|
MessagingMessagesImportJob.name,
|
||||||
{
|
{
|
||||||
workspaceId,
|
workspaceId: activeWorkspace.id,
|
||||||
messageChannelId: messageChannel.id,
|
messageChannelId: messageChannel.id,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,49 +1,43 @@
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
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 {
|
||||||
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
Workspace,
|
||||||
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
|
WorkspaceActivationStatus,
|
||||||
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
|
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator';
|
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 { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
|
||||||
import {
|
import {
|
||||||
MessagingOngoingStaleJobData,
|
|
||||||
MessagingOngoingStaleJob,
|
MessagingOngoingStaleJob,
|
||||||
|
MessagingOngoingStaleJobData,
|
||||||
} from 'src/modules/messaging/message-import-manager/jobs/messaging-ongoing-stale.job';
|
} 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)
|
@Processor(MessageQueue.cronQueue)
|
||||||
export class MessagingOngoingStaleCronJob {
|
export class MessagingOngoingStaleCronJob {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(DataSourceEntity, 'metadata')
|
@InjectRepository(Workspace, 'core')
|
||||||
private readonly dataSourceRepository: Repository<DataSourceEntity>,
|
private readonly workspaceRepository: Repository<Workspace>,
|
||||||
@InjectMessageQueue(MessageQueue.messagingQueue)
|
@InjectMessageQueue(MessageQueue.messagingQueue)
|
||||||
private readonly messageQueueService: MessageQueueService,
|
private readonly messageQueueService: MessageQueueService,
|
||||||
private readonly billingService: BillingService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Process(MessagingOngoingStaleCronJob.name)
|
@Process(MessagingOngoingStaleCronJob.name)
|
||||||
async handle(): Promise<void> {
|
async handle(): Promise<void> {
|
||||||
const workspaceIds =
|
const activeWorkspaces = await this.workspaceRepository.find({
|
||||||
await this.billingService.getActiveSubscriptionWorkspaceIds();
|
|
||||||
|
|
||||||
const dataSources = await this.dataSourceRepository.find({
|
|
||||||
where: {
|
where: {
|
||||||
workspaceId: In(workspaceIds),
|
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const workspaceIdsWithDataSources = new Set(
|
for (const activeWorkspace of activeWorkspaces) {
|
||||||
dataSources.map((dataSource) => dataSource.workspaceId),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const workspaceId of workspaceIdsWithDataSources) {
|
|
||||||
await this.messageQueueService.add<MessagingOngoingStaleJobData>(
|
await this.messageQueueService.add<MessagingOngoingStaleJobData>(
|
||||||
MessagingOngoingStaleJob.name,
|
MessagingOngoingStaleJob.name,
|
||||||
{
|
{
|
||||||
workspaceId,
|
workspaceId: activeWorkspace.id,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,14 +2,15 @@ import { Logger } from '@nestjs/common';
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import snakeCase from 'lodash.snakecase';
|
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 {
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
Workspace,
|
||||||
|
WorkspaceActivationStatus,
|
||||||
|
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
|
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
|
||||||
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
|
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
|
||||||
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
|
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 { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||||
import { MessageChannelRepository } from 'src/modules/messaging/common/repositories/message-channel.repository';
|
import { MessageChannelRepository } from 'src/modules/messaging/common/repositories/message-channel.repository';
|
||||||
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||||
@ -24,11 +25,8 @@ export class MessagingMessageChannelSyncStatusMonitoringCronJob {
|
|||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Workspace, 'core')
|
@InjectRepository(Workspace, 'core')
|
||||||
private readonly workspaceRepository: Repository<Workspace>,
|
private readonly workspaceRepository: Repository<Workspace>,
|
||||||
@InjectRepository(DataSourceEntity, 'metadata')
|
|
||||||
private readonly dataSourceRepository: Repository<DataSourceEntity>,
|
|
||||||
@InjectObjectMetadataRepository(MessageChannelWorkspaceEntity)
|
@InjectObjectMetadataRepository(MessageChannelWorkspaceEntity)
|
||||||
private readonly messageChannelRepository: MessageChannelRepository,
|
private readonly messageChannelRepository: MessageChannelRepository,
|
||||||
private readonly billingService: BillingService,
|
|
||||||
private readonly messagingTelemetryService: MessagingTelemetryService,
|
private readonly messagingTelemetryService: MessagingTelemetryService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -41,22 +39,16 @@ export class MessagingMessageChannelSyncStatusMonitoringCronJob {
|
|||||||
message: 'Starting message channel sync status monitoring',
|
message: 'Starting message channel sync status monitoring',
|
||||||
});
|
});
|
||||||
|
|
||||||
const workspaceIds =
|
const activeWorkspaces = await this.workspaceRepository.find({
|
||||||
await this.billingService.getActiveSubscriptionWorkspaceIds();
|
|
||||||
|
|
||||||
const dataSources = await this.dataSourceRepository.find({
|
|
||||||
where: {
|
where: {
|
||||||
workspaceId: In(workspaceIds),
|
activationStatus: WorkspaceActivationStatus.ACTIVE,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const workspaceIdsWithDataSources = new Set(
|
for (const activeWorkspace of activeWorkspaces) {
|
||||||
dataSources.map((dataSource) => dataSource.workspaceId),
|
const messageChannels = await this.messageChannelRepository.getAll(
|
||||||
);
|
activeWorkspace.id,
|
||||||
|
);
|
||||||
for (const workspaceId of workspaceIdsWithDataSources) {
|
|
||||||
const messageChannels =
|
|
||||||
await this.messageChannelRepository.getAll(workspaceId);
|
|
||||||
|
|
||||||
for (const messageChannel of messageChannels) {
|
for (const messageChannel of messageChannels) {
|
||||||
if (!messageChannel.syncStatus) {
|
if (!messageChannel.syncStatus) {
|
||||||
@ -66,7 +58,7 @@ export class MessagingMessageChannelSyncStatusMonitoringCronJob {
|
|||||||
eventName: `message_channel.monitoring.sync_status.${snakeCase(
|
eventName: `message_channel.monitoring.sync_status.${snakeCase(
|
||||||
messageChannel.syncStatus,
|
messageChannel.syncStatus,
|
||||||
)}`,
|
)}`,
|
||||||
workspaceId,
|
workspaceId: activeWorkspace.id,
|
||||||
connectedAccountId: messageChannel.connectedAccountId,
|
connectedAccountId: messageChannel.connectedAccountId,
|
||||||
messageChannelId: messageChannel.id,
|
messageChannelId: messageChannel.id,
|
||||||
message: messageChannel.syncStatus,
|
message: messageChannel.syncStatus,
|
||||||
|
|||||||
Reference in New Issue
Block a user