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:
@ -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,
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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',
|
||||
),
|
||||
],
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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' })
|
||||
|
||||
@ -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',
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
export enum OnboardingStep {
|
||||
SYNC_EMAIL = 'SYNC_EMAIL',
|
||||
INVITE_TEAM = 'INVITE_TEAM',
|
||||
}
|
||||
@ -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],
|
||||
})
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user