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:
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user