Update ChooseYourPlan page with new trial period options (#9628)
### Context - Update /plan-required page to let users get free trial without credit card plan - Update usePageChangeEffectNavigateLocation to redirect paused and canceled subscription (suspended workspace) to /settings/billing page ### To do - [x] Update usePageChangeEffectNavigateLocation test - [x] Update ChooseYourPlan sb test closes #9520 --------- Co-authored-by: etiennejouan <jouan.etienne@gmail.com>
This commit is contained in:
@ -0,0 +1,13 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { Min } from 'class-validator';
|
||||
|
||||
@ObjectType()
|
||||
export class TrialPeriodDTO {
|
||||
@Field(() => Number)
|
||||
@Min(0)
|
||||
duration: number;
|
||||
|
||||
@Field(() => Boolean)
|
||||
isCreditCardRequired: boolean;
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'class-validator';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
@ -50,15 +51,15 @@ export class BillingPortalWorkspaceService {
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
const stripeCustomerId = (
|
||||
await this.billingSubscriptionRepository.findOneBy({
|
||||
workspaceId: workspace.id,
|
||||
})
|
||||
)?.stripeCustomerId;
|
||||
const subscription = await this.billingSubscriptionRepository.findOneBy({
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
const session = await this.stripeCheckoutService.createCheckoutSession(
|
||||
const stripeCustomerId = subscription?.stripeCustomerId;
|
||||
|
||||
const session = await this.stripeCheckoutService.createCheckoutSession({
|
||||
user,
|
||||
workspace.id,
|
||||
workspaceId: workspace.id,
|
||||
priceId,
|
||||
quantity,
|
||||
successUrl,
|
||||
@ -66,7 +67,8 @@ export class BillingPortalWorkspaceService {
|
||||
stripeCustomerId,
|
||||
plan,
|
||||
requirePaymentMethod,
|
||||
);
|
||||
withTrialPeriod: !isDefined(subscription),
|
||||
});
|
||||
|
||||
assert(session.url, 'Error: missing checkout.session.url');
|
||||
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'class-validator';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
|
||||
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
|
||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
@ -16,15 +18,41 @@ export class BillingService {
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||
private readonly isFeatureEnabledService: FeatureFlagService,
|
||||
@InjectRepository(BillingSubscription, 'core')
|
||||
private readonly billingSubscriptionRepository: Repository<BillingSubscription>,
|
||||
) {}
|
||||
|
||||
isBillingEnabled() {
|
||||
return this.environmentService.get('IS_BILLING_ENABLED');
|
||||
}
|
||||
|
||||
async hasWorkspaceActiveSubscriptionOrFreeAccessOrEntitlement(
|
||||
async hasWorkspaceSubscriptionOrFreeAccess(workspaceId: string) {
|
||||
const isBillingEnabled = this.isBillingEnabled();
|
||||
|
||||
if (!isBillingEnabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const isFreeAccessEnabled =
|
||||
await this.isFeatureEnabledService.isFeatureEnabled(
|
||||
FeatureFlagKey.IsFreeAccessEnabled,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (isFreeAccessEnabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const subscription = await this.billingSubscriptionRepository.findOne({
|
||||
where: { workspaceId },
|
||||
});
|
||||
|
||||
return isDefined(subscription);
|
||||
}
|
||||
|
||||
async hasFreeAccessOrEntitlement(
|
||||
workspaceId: string,
|
||||
entitlementKey?: BillingEntitlementKey,
|
||||
entitlementKey: BillingEntitlementKey,
|
||||
) {
|
||||
const isBillingEnabled = this.isBillingEnabled();
|
||||
|
||||
@ -42,25 +70,9 @@ export class BillingService {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (entitlementKey) {
|
||||
return this.billingSubscriptionService.getWorkspaceEntitlementByKey(
|
||||
workspaceId,
|
||||
entitlementKey,
|
||||
);
|
||||
}
|
||||
|
||||
const currentBillingSubscription =
|
||||
await this.billingSubscriptionService.getCurrentBillingSubscriptionOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
return (
|
||||
isDefined(currentBillingSubscription) &&
|
||||
[
|
||||
SubscriptionStatus.Active,
|
||||
SubscriptionStatus.Trialing,
|
||||
SubscriptionStatus.PastDue,
|
||||
].includes(currentBillingSubscription.status)
|
||||
return this.billingSubscriptionService.getWorkspaceEntitlementByKey(
|
||||
workspaceId,
|
||||
entitlementKey,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,17 +24,29 @@ export class StripeCheckoutService {
|
||||
);
|
||||
}
|
||||
|
||||
async createCheckoutSession(
|
||||
user: User,
|
||||
workspaceId: string,
|
||||
priceId: string,
|
||||
quantity: number,
|
||||
successUrl?: string,
|
||||
cancelUrl?: string,
|
||||
stripeCustomerId?: string,
|
||||
plan: BillingPlanKey = BillingPlanKey.PRO,
|
||||
async createCheckoutSession({
|
||||
user,
|
||||
workspaceId,
|
||||
priceId,
|
||||
quantity,
|
||||
successUrl,
|
||||
cancelUrl,
|
||||
stripeCustomerId,
|
||||
plan = BillingPlanKey.PRO,
|
||||
requirePaymentMethod = true,
|
||||
): Promise<Stripe.Checkout.Session> {
|
||||
withTrialPeriod,
|
||||
}: {
|
||||
user: User;
|
||||
workspaceId: string;
|
||||
priceId: string;
|
||||
quantity: number;
|
||||
successUrl?: string;
|
||||
cancelUrl?: string;
|
||||
stripeCustomerId?: string;
|
||||
plan?: BillingPlanKey;
|
||||
requirePaymentMethod?: boolean;
|
||||
withTrialPeriod: boolean;
|
||||
}): Promise<Stripe.Checkout.Session> {
|
||||
return await this.stripe.checkout.sessions.create({
|
||||
line_items: [
|
||||
{
|
||||
@ -48,14 +60,25 @@ export class StripeCheckoutService {
|
||||
workspaceId,
|
||||
plan,
|
||||
},
|
||||
trial_period_days: this.environmentService.get(
|
||||
'BILLING_FREE_TRIAL_DURATION_IN_DAYS',
|
||||
),
|
||||
...(withTrialPeriod
|
||||
? {
|
||||
trial_period_days: this.environmentService.get(
|
||||
requirePaymentMethod
|
||||
? 'BILLING_FREE_TRIAL_WITH_CREDIT_CARD_DURATION_IN_DAYS'
|
||||
: 'BILLING_FREE_TRIAL_WITHOUT_CREDIT_CARD_DURATION_IN_DAYS',
|
||||
),
|
||||
trial_settings: {
|
||||
end_behavior: { missing_payment_method: 'pause' },
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
automatic_tax: { enabled: !!requirePaymentMethod },
|
||||
tax_id_collection: { enabled: !!requirePaymentMethod },
|
||||
customer: stripeCustomerId,
|
||||
customer_update: stripeCustomerId ? { name: 'auto' } : undefined,
|
||||
customer_update: stripeCustomerId
|
||||
? { name: 'auto', address: 'auto' }
|
||||
: undefined,
|
||||
customer_email: stripeCustomerId ? undefined : user.email,
|
||||
success_url: successUrl,
|
||||
cancel_url: cancelUrl,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { TrialPeriodDTO } from 'src/engine/core-modules/billing/dto/trial-period.dto';
|
||||
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
|
||||
import { AuthProviders } from 'src/engine/core-modules/workspace/dtos/public-workspace-data-output';
|
||||
|
||||
@ -11,8 +12,8 @@ class Billing {
|
||||
@Field(() => String, { nullable: true })
|
||||
billingUrl?: string;
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
billingFreeTrialDurationInDays?: number;
|
||||
@Field(() => [TrialPeriodDTO])
|
||||
trialPeriods: TrialPeriodDTO[];
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
|
||||
@ -18,9 +18,20 @@ export class ClientConfigResolver {
|
||||
billing: {
|
||||
isBillingEnabled: this.environmentService.get('IS_BILLING_ENABLED'),
|
||||
billingUrl: this.environmentService.get('BILLING_PLAN_REQUIRED_LINK'),
|
||||
billingFreeTrialDurationInDays: this.environmentService.get(
|
||||
'BILLING_FREE_TRIAL_DURATION_IN_DAYS',
|
||||
),
|
||||
trialPeriods: [
|
||||
{
|
||||
duration: this.environmentService.get(
|
||||
'BILLING_FREE_TRIAL_WITH_CREDIT_CARD_DURATION_IN_DAYS',
|
||||
),
|
||||
isCreditCardRequired: true,
|
||||
},
|
||||
{
|
||||
duration: this.environmentService.get(
|
||||
'BILLING_FREE_TRIAL_WITHOUT_CREDIT_CARD_DURATION_IN_DAYS',
|
||||
),
|
||||
isCreditCardRequired: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
authProviders: {
|
||||
google: this.environmentService.get('AUTH_GOOGLE_ENABLED'),
|
||||
|
||||
@ -73,7 +73,13 @@ export class EnvironmentVariables {
|
||||
@CastToPositiveNumber()
|
||||
@IsOptional()
|
||||
@ValidateIf((env) => env.IS_BILLING_ENABLED === true)
|
||||
BILLING_FREE_TRIAL_DURATION_IN_DAYS = 7;
|
||||
BILLING_FREE_TRIAL_WITH_CREDIT_CARD_DURATION_IN_DAYS = 30;
|
||||
|
||||
@IsNumber()
|
||||
@CastToPositiveNumber()
|
||||
@IsOptional()
|
||||
@ValidateIf((env) => env.IS_BILLING_ENABLED === true)
|
||||
BILLING_FREE_TRIAL_WITHOUT_CREDIT_CARD_DURATION_IN_DAYS = 7;
|
||||
|
||||
@IsString()
|
||||
@ValidateIf((env) => env.IS_BILLING_ENABLED === true)
|
||||
|
||||
@ -30,7 +30,7 @@ export class OnboardingService {
|
||||
|
||||
private async isSubscriptionIncompleteOnboardingStatus(workspace: Workspace) {
|
||||
const hasSubscription =
|
||||
await this.billingService.hasWorkspaceActiveSubscriptionOrFreeAccessOrEntitlement(
|
||||
await this.billingService.hasWorkspaceSubscriptionOrFreeAccess(
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
|
||||
@ -36,7 +36,7 @@ export class SSOService {
|
||||
|
||||
private async isSSOEnabled(workspaceId: string) {
|
||||
const isSSOBillingEnabled =
|
||||
await this.billingService.hasWorkspaceActiveSubscriptionOrFreeAccessOrEntitlement(
|
||||
await this.billingService.hasFreeAccessOrEntitlement(
|
||||
workspaceId,
|
||||
this.featureLookUpKey,
|
||||
);
|
||||
|
||||
@ -41,7 +41,10 @@ export class UserService extends TypeOrmQueryService<User> {
|
||||
}
|
||||
|
||||
async loadWorkspaceMember(user: User, workspace: Workspace) {
|
||||
if (workspace?.activationStatus !== WorkspaceActivationStatus.ACTIVE) {
|
||||
if (
|
||||
workspace?.activationStatus !== WorkspaceActivationStatus.ACTIVE &&
|
||||
workspace?.activationStatus !== WorkspaceActivationStatus.SUSPENDED
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user