5095 move onboardingstatus computation from frontend to backend (#5954)

- move front `onboardingStatus` computing to server side
- add logic to `useSetNextOnboardingStatus`
- update some missing redirections in
`usePageChangeEffectNavigateLocation`
- separate subscriptionStatus from onboardingStatus
This commit is contained in:
martmull
2024-06-28 17:32:02 +02:00
committed by GitHub
parent 1a66db5bff
commit b8f33f6f59
78 changed files with 1767 additions and 1763 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,61 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class UseEnumForSubscriptionStatusInterval1719327438923
implements MigrationInterface
{
name = 'UseEnumForSubscriptionStatusInterval1719327438923';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TYPE "core"."billingSubscription_status_enum" AS ENUM('active', 'canceled', 'incomplete', 'incomplete_expired', 'past_due', 'paused', 'trialing', 'unpaid')`,
);
await queryRunner.query(
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "status" TYPE "core"."billingSubscription_status_enum" USING "status"::"core"."billingSubscription_status_enum"`,
);
await queryRunner.query(
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "status" SET NOT NULL`,
);
await queryRunner.query(
`CREATE TYPE "core"."billingSubscription_interval_enum" AS ENUM('day', 'month', 'week', 'year')`,
);
await queryRunner.query(
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "interval" TYPE "core"."billingSubscription_interval_enum" USING "interval"::"core"."billingSubscription_interval_enum"`,
);
await queryRunner.query(
`CREATE TYPE "core"."workspace_subscriptionstatus_enum" AS ENUM('active', 'canceled', 'incomplete', 'incomplete_expired', 'past_due', 'paused', 'trialing', 'unpaid')`,
);
await queryRunner.query(
`ALTER TABLE "core"."workspace" ALTER COLUMN "subscriptionStatus" DROP DEFAULT`,
);
await queryRunner.query(
`ALTER TABLE "core"."workspace" ALTER COLUMN "subscriptionStatus" TYPE "core"."workspace_subscriptionstatus_enum" USING "subscriptionStatus"::"core"."workspace_subscriptionstatus_enum"`,
);
await queryRunner.query(
`ALTER TABLE "core"."workspace" ALTER COLUMN "subscriptionStatus" SET NOT NULL`,
);
await queryRunner.query(
`ALTER TABLE "core"."workspace" ALTER COLUMN "subscriptionStatus" SET DEFAULT 'incomplete'::"core"."workspace_subscriptionstatus_enum"`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."workspace" ALTER COLUMN "subscriptionStatus" TYPE text`,
);
await queryRunner.query(
`DROP TYPE "core"."workspace_subscriptionstatus_enum"`,
);
await queryRunner.query(
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "interval" TYPE text`,
);
await queryRunner.query(
`DROP TYPE "core"."billingSubscription_interval_enum"`,
);
await queryRunner.query(
`ALTER TABLE "core"."billingSubscription" ALTER COLUMN "status" TYPE text`,
);
await queryRunner.query(
`DROP TYPE "core"."billingSubscription_status_enum"`,
);
}
}

View File

@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class RemoveSubscriptionStatusFromCoreWorkspace1719494707738
implements MigrationInterface
{
name = 'RemoveSubscriptionStatusFromCoreWorkspace1719494707738';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."workspace" DROP COLUMN "subscriptionStatus"`,
);
await queryRunner.query(
`DROP TYPE "core"."workspace_subscriptionstatus_enum"`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TYPE "core"."workspace_subscriptionstatus_enum" AS ENUM('active', 'canceled', 'incomplete', 'incomplete_expired', 'past_due', 'paused', 'trialing', 'unpaid')`,
);
await queryRunner.query(
`ALTER TABLE "core"."workspace" ADD "subscriptionStatus" "core"."workspace_subscriptionstatus_enum" NOT NULL DEFAULT 'incomplete'`,
);
}
}

View File

@ -30,6 +30,7 @@ import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/stan
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
import { AuthResolver } from './auth.resolver';
@ -65,6 +66,7 @@ const jwtModule = JwtModule.registerAsync({
]),
HttpModule,
UserWorkspaceModule,
WorkspaceModule,
OnboardingModule,
TwentyORMModule.forFeature([CalendarChannelWorkspaceEntity]),
WorkspaceDataSourceModule,

View File

@ -8,6 +8,7 @@ import { EnvironmentService } from 'src/engine/integrations/environment/environm
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.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 { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
describe('SignInUpService', () => {
let service: SignInUpService;
@ -40,6 +41,10 @@ describe('SignInUpService', () => {
provide: HttpService,
useValue: {},
},
{
provide: WorkspaceService,
useValue: {},
},
],
}).compile();

View File

@ -24,6 +24,7 @@ import { FileUploadService } from 'src/engine/core-modules/file/file-upload/serv
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { getImageBufferFromUrl } from 'src/utils/image';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
export type SignInUpServiceInput = {
email: string;
@ -44,6 +45,7 @@ export class SignInUpService {
@InjectRepository(User, 'core')
private readonly userRepository: Repository<User>,
private readonly userWorkspaceService: UserWorkspaceService,
private readonly workspaceService: WorkspaceService,
private readonly httpService: HttpService,
private readonly environmentService: EnvironmentService,
) {}
@ -142,10 +144,12 @@ export class SignInUpService {
ForbiddenException,
);
const isWorkspaceActivated =
await this.workspaceService.isWorkspaceActivated(workspace.id);
assert(
!this.environmentService.get('IS_BILLING_ENABLED') ||
workspace.subscriptionStatus !== 'incomplete',
'Workspace subscription status is incomplete',
isWorkspaceActivated,
'Workspace is not ready to welcome new members',
ForbiddenException,
);
@ -199,7 +203,6 @@ export class SignInUpService {
displayName: '',
domainName: '',
inviteHash: v4(),
subscriptionStatus: 'incomplete',
});
const workspace = await this.workspaceRepository.save(workspaceToCreate);

View File

@ -10,13 +10,19 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { BillingResolver } from 'src/engine/core-modules/billing/billing.resolver';
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 { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
@Module({
imports: [
StripeModule,
UserWorkspaceModule,
TypeOrmModule.forFeature(
[BillingSubscription, BillingSubscriptionItem, Workspace],
[
BillingSubscription,
BillingSubscriptionItem,
Workspace,
FeatureFlagEntity,
],
'core',
),
],

View File

@ -2,17 +2,25 @@ import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import Stripe from 'stripe';
import { Not, Repository } from 'typeorm';
import { In, Not, Repository } from 'typeorm';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import {
BillingSubscription,
SubscriptionInterval,
SubscriptionStatus,
} 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 { ProductPriceEntity } from 'src/engine/core-modules/billing/dto/product-price.entity';
import { User } from 'src/engine/core-modules/user/user.entity';
import { assert } from 'src/utils/assert';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import {
FeatureFlagEntity,
FeatureFlagKeys,
} from 'src/engine/core-modules/feature-flag/feature-flag.entity';
export enum AvailableProduct {
BasePlan = 'base-plan',
@ -34,12 +42,45 @@ export class BillingService {
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 getActiveSubscriptionWorkspaceIds() {
return (
await this.workspaceRepository.find({
where: this.environmentService.get('IS_BILLING_ENABLED')
? {
currentBillingSubscription: {
status: In([
SubscriptionStatus.Active,
SubscriptionStatus.Trialing,
SubscriptionStatus.PastDue,
]),
},
}
: {},
select: ['id'],
})
).map((workspace) => workspace.id);
}
async isBillingEnabledForWorkspace(workspaceId: string) {
const isFreeAccessEnabled = await this.featureFlagRepository.findOneBy({
workspaceId,
key: FeatureFlagKeys.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');
@ -84,13 +125,13 @@ export class BillingService {
}) {
const notCanceledSubscriptions =
await this.billingSubscriptionRepository.find({
where: { ...criteria, status: Not('canceled') },
where: { ...criteria, status: Not(SubscriptionStatus.Canceled) },
relations: ['billingSubscriptionItems'],
});
assert(
notCanceledSubscriptions.length <= 1,
`More than on not canceled subscription for workspace ${criteria.workspaceId}`,
`More than one not canceled subscription for workspace ${criteria.workspaceId}`,
);
return notCanceledSubscriptions?.[0];
@ -171,7 +212,9 @@ export class BillingService {
workspaceId: user.defaultWorkspaceId,
});
const newInterval =
billingSubscription?.interval === 'year' ? 'month' : 'year';
billingSubscription?.interval === SubscriptionInterval.Year
? SubscriptionInterval.Month
: SubscriptionInterval.Year;
const billingSubscriptionItem = await this.getBillingSubscriptionItem(
user.defaultWorkspaceId,
);
@ -265,10 +308,6 @@ export class BillingService {
return;
}
await this.workspaceRepository.update(workspaceId, {
subscriptionStatus: data.object.status,
});
await this.billingSubscriptionRepository.upsert(
{
workspaceId: workspaceId,
@ -302,5 +341,10 @@ export class BillingService {
skipUpdateIfNoValuesChanged: true,
},
);
await this.featureFlagRepository.delete({
workspaceId,
key: FeatureFlagKeys.IsFreeAccessEnabled,
});
}
}

View File

@ -3,9 +3,11 @@ import { ArgsType, Field } from '@nestjs/graphql';
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
import Stripe from 'stripe';
import { SubscriptionInterval } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
@ArgsType()
export class CheckoutSessionInput {
@Field(() => String)
@Field(() => SubscriptionInterval)
@IsString()
@IsNotEmpty()
recurringInterval: Stripe.Price.Recurring.Interval;

View File

@ -2,9 +2,11 @@ import { Field, ObjectType } from '@nestjs/graphql';
import Stripe from 'stripe';
import { SubscriptionInterval } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
@ObjectType()
export class ProductPriceEntity {
@Field(() => String)
@Field(() => SubscriptionInterval)
recurringInterval: Stripe.Price.Recurring.Interval;
@Field(() => Number)

View File

@ -1,4 +1,4 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import {
Column,
@ -18,6 +18,27 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
export enum SubscriptionStatus {
Active = 'active',
Canceled = 'canceled',
Incomplete = 'incomplete',
IncompleteExpired = 'incomplete_expired',
PastDue = 'past_due',
Paused = 'paused',
Trialing = 'trialing',
Unpaid = 'unpaid',
}
export enum SubscriptionInterval {
Day = 'day',
Month = 'month',
Week = 'week',
Year = 'year',
}
registerEnumType(SubscriptionStatus, { name: 'SubscriptionStatus' });
registerEnumType(SubscriptionInterval, { name: 'SubscriptionInterval' });
@Entity({ name: 'billingSubscription', schema: 'core' })
@ObjectType('BillingSubscription')
export class BillingSubscription {
@ -49,12 +70,20 @@ export class BillingSubscription {
@Column({ unique: true, nullable: false })
stripeSubscriptionId: string;
@Field(() => String)
@Column({ type: 'text', nullable: false })
@Field(() => SubscriptionStatus)
@Column({
type: 'enum',
enum: Object.values(SubscriptionStatus),
nullable: false,
})
status: Stripe.Subscription.Status;
@Field(() => String, { nullable: true })
@Column({ type: 'text', nullable: true })
@Field(() => SubscriptionInterval, { nullable: true })
@Column({
type: 'enum',
enum: Object.values(SubscriptionInterval),
nullable: true,
})
interval: Stripe.Price.Recurring.Interval;
@OneToMany(

View File

@ -9,15 +9,15 @@ import {
UpdateSubscriptionJob,
UpdateSubscriptionJobData,
} from 'src/engine/core-modules/billing/jobs/update-subscription.job';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator';
import { BillingService } from 'src/engine/core-modules/billing/billing.service';
@Injectable()
export class BillingWorkspaceMemberListener {
constructor(
@InjectMessageQueue(MessageQueue.billingQueue)
private readonly messageQueueService: MessageQueueService,
private readonly environmentService: EnvironmentService,
private readonly billingService: BillingService,
) {}
@OnEvent('workspaceMember.created')
@ -25,7 +25,12 @@ export class BillingWorkspaceMemberListener {
async handleCreateOrDeleteEvent(
payload: ObjectRecordCreateEvent<WorkspaceMemberWorkspaceEntity>,
) {
if (!this.environmentService.get('IS_BILLING_ENABLED')) {
const isBillingEnabledForWorkspace =
await this.billingService.isBillingEnabledForWorkspace(
payload.workspaceId,
);
if (!isBillingEnabledForWorkspace) {
return;
}

View File

@ -23,6 +23,7 @@ export enum FeatureFlagKeys {
IsStripeIntegrationEnabled = 'IS_STRIPE_INTEGRATION_ENABLED',
IsContactCreationForSentAndReceivedEmailsEnabled = 'IS_CONTACT_CREATION_FOR_SENT_AND_RECEIVED_EMAILS_ENABLED',
IsGoogleCalendarSyncV2Enabled = 'IS_GOOGLE_CALENDAR_SYNC_V2_ENABLED',
IsFreeAccessEnabled = 'IS_FREE_ACCESS_ENABLED',
}
@Entity({ name: 'featureFlag', schema: 'core' })

View File

@ -0,0 +1,8 @@
export enum OnboardingStatus {
PLAN_REQUIRED = 'PLAN_REQUIRED',
WORKSPACE_ACTIVATION = 'WORKSPACE_ACTIVATION',
PROFILE_CREATION = 'PROFILE_CREATION',
SYNC_EMAIL = 'SYNC_EMAIL',
INVITE_TEAM = 'INVITE_TEAM',
COMPLETED = 'COMPLETED',
}

View File

@ -1,4 +0,0 @@
export enum OnboardingStep {
SYNC_EMAIL = 'SYNC_EMAIL',
INVITE_TEAM = 'INVITE_TEAM',
}

View File

@ -5,9 +5,19 @@ import { OnboardingResolver } from 'src/engine/core-modules/onboarding/onboardin
import { KeyValuePairModule } from 'src/engine/core-modules/key-value-pair/key-value-pair.module';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
import { EnvironmentModule } from 'src/engine/integrations/environment/environment.module';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
@Module({
imports: [DataSourceModule, UserWorkspaceModule, KeyValuePairModule],
imports: [
DataSourceModule,
WorkspaceManagerModule,
UserWorkspaceModule,
KeyValuePairModule,
EnvironmentModule,
BillingModule,
],
exports: [OnboardingService],
providers: [OnboardingService, OnboardingResolver],
})

View File

@ -1,13 +1,20 @@
import { Injectable } from '@nestjs/common';
import { KeyValuePairService } from 'src/engine/core-modules/key-value-pair/key-value-pair.service';
import { OnboardingStep } from 'src/engine/core-modules/onboarding/enums/onboarding-step.enum';
import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
import { InjectWorkspaceRepository } from 'src/engine/twenty-orm/decorators/inject-workspace-repository.decorator';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { SubscriptionStatus } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { isDefined } from 'src/utils/is-defined';
import { BillingService } from 'src/engine/core-modules/billing/billing.service';
enum OnboardingStepValues {
SKIPPED = 'SKIPPED',
@ -26,29 +33,71 @@ type OnboardingKeyValueType = {
@Injectable()
export class OnboardingService {
constructor(
private readonly billingService: BillingService,
private readonly workspaceManagerService: WorkspaceManagerService,
private readonly userWorkspaceService: UserWorkspaceService,
private readonly keyValuePairService: KeyValuePairService<OnboardingKeyValueType>,
@InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity)
private readonly connectedAccountRepository: ConnectedAccountRepository,
@InjectWorkspaceRepository(WorkspaceMemberWorkspaceEntity)
private readonly workspaceMemberRepository: WorkspaceRepository<WorkspaceMemberWorkspaceEntity>,
) {}
private async isSyncEmailOnboardingStep(user: User, workspace: Workspace) {
private async isSubscriptionIncompleteOnboardingStatus(user: User) {
const isBillingEnabledForWorkspace =
await this.billingService.isBillingEnabledForWorkspace(
user.defaultWorkspaceId,
);
if (!isBillingEnabledForWorkspace) {
return false;
}
const currentBillingSubscription =
await this.billingService.getCurrentBillingSubscription({
workspaceId: user.defaultWorkspaceId,
});
return (
!isDefined(currentBillingSubscription) ||
currentBillingSubscription?.status === SubscriptionStatus.Incomplete
);
}
private async isWorkspaceActivationOnboardingStatus(user: User) {
return !(await this.workspaceManagerService.doesDataSourceExist(
user.defaultWorkspaceId,
));
}
private async isProfileCreationOnboardingStatus(user: User) {
const workspaceMember = await this.workspaceMemberRepository.findOneBy({
userId: user.id,
});
return (
workspaceMember &&
(!workspaceMember.name.firstName || !workspaceMember.name.lastName)
);
}
private async isSyncEmailOnboardingStatus(user: User) {
const syncEmailValue = await this.keyValuePairService.get({
userId: user.id,
workspaceId: workspace.id,
workspaceId: user.defaultWorkspaceId,
key: OnboardingStepKeys.SYNC_EMAIL_ONBOARDING_STEP,
});
const isSyncEmailSkipped = syncEmailValue === OnboardingStepValues.SKIPPED;
const connectedAccounts =
await this.connectedAccountRepository.getAllByUserId(
user.id,
workspace.id,
user.defaultWorkspaceId,
);
return !isSyncEmailSkipped && !connectedAccounts?.length;
}
private async isInviteTeamOnboardingStep(workspace: Workspace) {
private async isInviteTeamOnboardingStatus(workspace: Workspace) {
const inviteTeamValue = await this.keyValuePairService.get({
workspaceId: workspace.id,
key: OnboardingStepKeys.INVITE_TEAM_ONBOARDING_STEP,
@ -64,19 +113,28 @@ export class OnboardingService {
);
}
async getOnboardingStep(
user: User,
workspace: Workspace,
): Promise<OnboardingStep | null> {
if (await this.isSyncEmailOnboardingStep(user, workspace)) {
return OnboardingStep.SYNC_EMAIL;
async getOnboardingStatus(user: User) {
if (await this.isSubscriptionIncompleteOnboardingStatus(user)) {
return OnboardingStatus.PLAN_REQUIRED;
}
if (await this.isInviteTeamOnboardingStep(workspace)) {
return OnboardingStep.INVITE_TEAM;
if (await this.isWorkspaceActivationOnboardingStatus(user)) {
return OnboardingStatus.WORKSPACE_ACTIVATION;
}
return null;
if (await this.isProfileCreationOnboardingStatus(user)) {
return OnboardingStatus.PROFILE_CREATION;
}
if (await this.isSyncEmailOnboardingStatus(user)) {
return OnboardingStatus.SYNC_EMAIL;
}
if (await this.isInviteTeamOnboardingStatus(user.defaultWorkspace)) {
return OnboardingStatus.INVITE_TEAM;
}
return OnboardingStatus.COMPLETED;
}
async skipInviteTeamOnboardingStep(workspaceId: string) {

View File

@ -107,9 +107,7 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
return undefined;
}
const workspaceMemberCount = await this.workspaceMemberRepository.count();
return workspaceMemberCount;
return await this.workspaceMemberRepository.count();
}
async checkUserWorkspaceExists(

View File

@ -18,11 +18,11 @@ import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-mem
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
import { OnboardingStep } from 'src/engine/core-modules/onboarding/enums/onboarding-step.enum';
import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
registerEnumType(OnboardingStep, {
name: 'OnboardingStep',
description: 'Onboarding step',
registerEnumType(OnboardingStatus, {
name: 'OnboardingStatus',
description: 'Onboarding status',
});
@Entity({ name: 'user', schema: 'core' })
@ -119,6 +119,6 @@ export class User {
@OneToMany(() => UserWorkspace, (userWorkspace) => userWorkspace.user)
workspaces: Relation<UserWorkspace[]>;
@Field(() => OnboardingStep, { nullable: true })
onboardingStep: OnboardingStep;
@Field(() => OnboardingStatus, { nullable: true })
onboardingStatus: OnboardingStatus;
}

View File

@ -27,7 +27,7 @@ import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
import { User } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
import { OnboardingStep } from 'src/engine/core-modules/onboarding/enums/onboarding-step.enum';
import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { LoadServiceWithWorkspaceContext } from 'src/engine/twenty-orm/context/load-service-with-workspace.context';
@ -118,17 +118,13 @@ export class UserResolver {
return this.userService.deleteUser(userId);
}
@ResolveField(() => OnboardingStep)
async onboardingStep(@Parent() user: User): Promise<OnboardingStep | null> {
if (!user) {
return null;
}
@ResolveField(() => OnboardingStatus)
async onboardingStatus(@Parent() user: User): Promise<OnboardingStatus> {
const contextInstance = await this.loadServiceWithWorkspaceContext.load(
this.onboardingService,
user.defaultWorkspaceId,
);
return contextInstance.getOnboardingStep(user, user.defaultWorkspace);
return contextInstance.getOnboardingStatus(user);
}
}

View File

@ -10,7 +10,6 @@ import {
Relation,
UpdateDateColumn,
} from 'typeorm';
import Stripe from 'stripe';
import { User } from 'src/engine/core-modules/user/user.entity';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
@ -85,10 +84,6 @@ export class Workspace {
@OneToMany(() => FeatureFlagEntity, (featureFlag) => featureFlag.workspace)
featureFlags: Relation<FeatureFlagEntity[]>;
@Field(() => String)
@Column({ type: 'text', default: 'incomplete' })
subscriptionStatus: Stripe.Subscription.Status;
@Field({ nullable: true })
currentBillingSubscription: BillingSubscription;

View File

@ -8,6 +8,7 @@ import { WorkspaceService } from 'src/engine/core-modules/workspace/services/wor
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { getDryRunLogHeader } from 'src/utils/get-dry-run-log-header';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { SubscriptionStatus } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
type DeleteIncompleteWorkspacesCommandOptions = {
dryRun?: boolean;
@ -52,7 +53,7 @@ export class DeleteIncompleteWorkspacesCommand extends CommandRunner {
options: DeleteIncompleteWorkspacesCommandOptions,
): Promise<void> {
const where: FindOptionsWhere<Workspace> = {
subscriptionStatus: 'incomplete',
currentBillingSubscription: { status: SubscriptionStatus.Incomplete },
};
if (options.workspaceIds) {

View File

@ -60,6 +60,7 @@ export class AddStandardIdCommand extends CommandRunner {
IS_STRIPE_INTEGRATION_ENABLED: false,
IS_CONTACT_CREATION_FOR_SENT_AND_RECEIVED_EMAILS_ENABLED: true,
IS_GOOGLE_CALENDAR_SYNC_V2_ENABLED: true,
IS_FREE_ACCESS_ENABLED: false,
},
);
const standardFieldMetadataCollection = this.standardFieldFactory.create(
@ -76,6 +77,7 @@ export class AddStandardIdCommand extends CommandRunner {
IS_STRIPE_INTEGRATION_ENABLED: false,
IS_CONTACT_CREATION_FOR_SENT_AND_RECEIVED_EMAILS_ENABLED: true,
IS_GOOGLE_CALENDAR_SYNC_V2_ENABLED: true,
IS_FREE_ACCESS_ENABLED: false,
},
);

View File

@ -2,16 +2,17 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { GoogleCalendarSyncCronJob } from 'src/modules/calendar/crons/jobs/google-calendar-sync.cron.job';
import { WorkspaceGoogleCalendarSyncModule } from 'src/modules/calendar/services/workspace-google-calendar-sync/workspace-google-calendar-sync.module';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
@Module({
imports: [
TypeOrmModule.forFeature([Workspace, FeatureFlagEntity], 'core'),
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
TypeOrmModule.forFeature([DataSourceEntity], 'metadata'),
WorkspaceGoogleCalendarSyncModule,
BillingModule,
],
providers: [GoogleCalendarSyncCronJob],
})

View File

@ -3,13 +3,12 @@ import { Scope } from '@nestjs/common';
import { Repository, In } from 'typeorm';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { WorkspaceGoogleCalendarSyncService } from 'src/modules/calendar/services/workspace-google-calendar-sync/workspace-google-calendar-sync.service';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
import { BillingService } from 'src/engine/core-modules/billing/billing.service';
@Processor({
queueName: MessageQueue.cronQueue,
@ -17,26 +16,16 @@ import { Process } from 'src/engine/integrations/message-queue/decorators/proces
})
export class GoogleCalendarSyncCronJob {
constructor(
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(DataSourceEntity, 'metadata')
private readonly dataSourceRepository: Repository<DataSourceEntity>,
private readonly workspaceGoogleCalendarSyncService: WorkspaceGoogleCalendarSyncService,
private readonly environmentService: EnvironmentService,
private readonly billingService: BillingService,
) {}
@Process(GoogleCalendarSyncCronJob.name)
async handle(): Promise<void> {
const workspaceIds = (
await this.workspaceRepository.find({
where: this.environmentService.get('IS_BILLING_ENABLED')
? {
subscriptionStatus: In(['active', 'trialing', 'past_due']),
}
: {},
select: ['id'],
})
).map((workspace) => workspace.id);
const workspaceIds =
await this.billingService.getActiveSubscriptionWorkspaceIds();
const dataSources = await this.dataSourceRepository.find({
where: {

View File

@ -3,12 +3,10 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository, In } from 'typeorm';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { MessageChannelRepository } from 'src/modules/messaging/common/repositories/message-channel.repository';
import {
MessageChannelSyncStage,
@ -21,35 +19,26 @@ import {
import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decorators/message-queue.decorator';
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
import { BillingService } from 'src/engine/core-modules/billing/billing.service';
@Processor(MessageQueue.cronQueue)
export class MessagingMessageListFetchCronJob {
private readonly logger = new Logger(MessagingMessageListFetchCronJob.name);
constructor(
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(DataSourceEntity, 'metadata')
private readonly dataSourceRepository: Repository<DataSourceEntity>,
@InjectMessageQueue(MessageQueue.messagingQueue)
private readonly messageQueueService: MessageQueueService,
@InjectObjectMetadataRepository(MessageChannelWorkspaceEntity)
private readonly messageChannelRepository: MessageChannelRepository,
private readonly environmentService: EnvironmentService,
private readonly billingService: BillingService,
) {}
@Process(MessagingMessageListFetchCronJob.name)
async handle(): Promise<void> {
const workspaceIds = (
await this.workspaceRepository.find({
where: this.environmentService.get('IS_BILLING_ENABLED')
? {
subscriptionStatus: In(['active', 'trialing', 'past_due']),
}
: {},
select: ['id'],
})
).map((workspace) => workspace.id);
const workspaceIds =
await this.billingService.getActiveSubscriptionWorkspaceIds();
const dataSources = await this.dataSourceRepository.find({
where: {

View File

@ -3,9 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository, In } from 'typeorm';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.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 {
@ -21,35 +19,26 @@ import {
MessageChannelSyncStage,
MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { BillingService } from 'src/engine/core-modules/billing/billing.service';
@Processor(MessageQueue.cronQueue)
export class MessagingMessagesImportCronJob {
private readonly logger = new Logger(MessagingMessagesImportCronJob.name);
constructor(
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(DataSourceEntity, 'metadata')
private readonly dataSourceRepository: Repository<DataSourceEntity>,
private readonly environmentService: EnvironmentService,
@InjectMessageQueue(MessageQueue.messagingQueue)
private readonly messageQueueService: MessageQueueService,
@InjectObjectMetadataRepository(MessageChannelWorkspaceEntity)
private readonly messageChannelRepository: MessageChannelRepository,
private readonly billingService: BillingService,
) {}
@Process(MessagingMessagesImportCronJob.name)
async handle(): Promise<void> {
const workspaceIds = (
await this.workspaceRepository.find({
where: this.environmentService.get('IS_BILLING_ENABLED')
? {
subscriptionStatus: In(['active', 'trialing', 'past_due']),
}
: {},
select: ['id'],
})
).map((workspace) => workspace.id);
const workspaceIds =
await this.billingService.getActiveSubscriptionWorkspaceIds();
const dataSources = await this.dataSourceRepository.find({
where: {

View File

@ -2,9 +2,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository, In } from 'typeorm';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
import { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
@ -14,31 +12,22 @@ import {
MessagingOngoingStaleJobData,
MessagingOngoingStaleJob,
} from 'src/modules/messaging/message-import-manager/jobs/messaging-ongoing-stale.job';
import { BillingService } from 'src/engine/core-modules/billing/billing.service';
@Processor(MessageQueue.cronQueue)
export class MessagingOngoingStaleCronJob {
constructor(
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(DataSourceEntity, 'metadata')
private readonly dataSourceRepository: Repository<DataSourceEntity>,
private readonly environmentService: EnvironmentService,
@InjectMessageQueue(MessageQueue.messagingQueue)
private readonly messageQueueService: MessageQueueService,
private readonly billingService: BillingService,
) {}
@Process(MessagingOngoingStaleCronJob.name)
async handle(): Promise<void> {
const workspaceIds = (
await this.workspaceRepository.find({
where: this.environmentService.get('IS_BILLING_ENABLED')
? {
subscriptionStatus: In(['active', 'trialing', 'past_due']),
}
: {},
select: ['id'],
})
).map((workspace) => workspace.id);
const workspaceIds =
await this.billingService.getActiveSubscriptionWorkspaceIds();
const dataSources = await this.dataSourceRepository.find({
where: {

View File

@ -20,6 +20,7 @@ import { MessagingMessageListFetchJob } from 'src/modules/messaging/message-impo
import { MessagingMessagesImportJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job';
import { MessagingOngoingStaleJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-ongoing-stale.job';
import { MessagingMessageImportManagerMessageChannelListener } from 'src/modules/messaging/message-import-manager/listeners/messaging-import-manager-message-channel.listener';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
@Module({
imports: [
@ -28,6 +29,7 @@ import { MessagingMessageImportManagerMessageChannelListener } from 'src/modules
TypeOrmModule.forFeature([Workspace], 'core'),
TypeOrmModule.forFeature([DataSourceEntity], 'metadata'),
TwentyORMModule.forFeature([MessageChannelWorkspaceEntity]),
BillingModule,
],
providers: [
MessagingMessageListFetchCronCommand,

View File

@ -8,12 +8,12 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
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 { Processor } from 'src/engine/integrations/message-queue/decorators/processor.decorator';
import { Process } from 'src/engine/integrations/message-queue/decorators/process.decorator';
import { MessagingTelemetryService } from 'src/modules/messaging/common/services/messaging-telemetry.service';
import { BillingService } from 'src/engine/core-modules/billing/billing.service';
@Processor(MessageQueue.cronQueue)
export class MessagingMessageChannelSyncStatusMonitoringCronJob {
@ -28,7 +28,7 @@ export class MessagingMessageChannelSyncStatusMonitoringCronJob {
private readonly dataSourceRepository: Repository<DataSourceEntity>,
@InjectObjectMetadataRepository(MessageChannelWorkspaceEntity)
private readonly messageChannelRepository: MessageChannelRepository,
private readonly environmentService: EnvironmentService,
private readonly billingService: BillingService,
private readonly messagingTelemetryService: MessagingTelemetryService,
) {}
@ -41,16 +41,8 @@ export class MessagingMessageChannelSyncStatusMonitoringCronJob {
message: 'Starting message channel sync status monitoring',
});
const workspaceIds = (
await this.workspaceRepository.find({
where: this.environmentService.get('IS_BILLING_ENABLED')
? {
subscriptionStatus: In(['active', 'trialing', 'past_due']),
}
: {},
select: ['id'],
})
).map((workspace) => workspace.id);
const workspaceIds =
await this.billingService.getActiveSubscriptionWorkspaceIds();
const dataSources = await this.dataSourceRepository.find({
where: {

View File

@ -6,10 +6,12 @@ import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-s
import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module';
import { MessagingMessageChannelSyncStatusMonitoringCronCommand } from 'src/modules/messaging/monitoring/crons/commands/messaging-message-channel-sync-status-monitoring.cron.command';
import { MessagingMessageChannelSyncStatusMonitoringCronJob } from 'src/modules/messaging/monitoring/crons/jobs/messaging-message-channel-sync-status-monitoring.cron';
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
@Module({
imports: [
MessagingCommonModule,
BillingModule,
TypeOrmModule.forFeature([Workspace], 'core'),
TypeOrmModule.forFeature([DataSourceEntity], 'metadata'),
],

View File

@ -11,22 +11,6 @@ export class WorkspaceMemberRepository {
private readonly workspaceDataSourceService: WorkspaceDataSourceService,
) {}
public async getByIds(
userIds: string[],
workspaceId: string,
): Promise<WorkspaceMemberWorkspaceEntity[]> {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);
const result = await this.workspaceDataSourceService.executeRawQuery(
`SELECT * FROM ${dataSourceSchema}."workspaceMember" WHERE "userId" = ANY($1)`,
[userIds],
workspaceId,
);
return result;
}
public async find(workspaceMemberId: string, workspaceId: string) {
const dataSourceSchema =
this.workspaceDataSourceService.getSchemaName(workspaceId);