add trial period ending banner + server logic (#11389)
Solution : - if user reaches his workflow usage cap + in trial period > display banner - end trial period if user has payment method (if not, not possible) <img width="941" alt="Screenshot 2025-04-04 at 10 27 32" src="https://github.com/user-attachments/assets/d7a1d5f7-9b12-4a92-a7c7-15ef8847c790" />
This commit is contained in:
@ -19,4 +19,5 @@ export enum BillingExceptionCode {
|
||||
BILLING_MISSING_REQUEST_BODY = 'BILLING_MISSING_REQUEST_BODY',
|
||||
BILLING_UNHANDLED_ERROR = 'BILLING_UNHANDLED_ERROR',
|
||||
BILLING_STRIPE_ERROR = 'BILLING_STRIPE_ERROR',
|
||||
BILLING_SUBSCRIPTION_NOT_IN_TRIAL_PERIOD = 'BILLING_SUBSCRIPTION_NOT_IN_TRIAL_PERIOD',
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { BillingCheckoutSessionInput } from 'src/engine/core-modules/billing/dtos/inputs/billing-checkout-session.input';
|
||||
import { BillingSessionInput } from 'src/engine/core-modules/billing/dtos/inputs/billing-session.input';
|
||||
import { BillingEndTrialPeriodOutput } from 'src/engine/core-modules/billing/dtos/outputs/billing-end-trial-period.output';
|
||||
import { BillingPlanOutput } from 'src/engine/core-modules/billing/dtos/outputs/billing-plan.output';
|
||||
import { BillingSessionOutput } from 'src/engine/core-modules/billing/dtos/outputs/billing-session.output';
|
||||
import { BillingUpdateOutput } from 'src/engine/core-modules/billing/dtos/outputs/billing-update.output';
|
||||
@ -130,6 +131,17 @@ export class BillingResolver {
|
||||
return plans.map(formatBillingDatabaseProductToGraphqlDTO);
|
||||
}
|
||||
|
||||
@Mutation(() => BillingEndTrialPeriodOutput)
|
||||
@UseGuards(
|
||||
WorkspaceAuthGuard,
|
||||
SettingsPermissionsGuard(SettingPermissionType.WORKSPACE),
|
||||
)
|
||||
async endSubscriptionTrialPeriod(
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
): Promise<BillingEndTrialPeriodOutput> {
|
||||
return await this.billingSubscriptionService.endTrialPeriod(workspace);
|
||||
}
|
||||
|
||||
private async validateCanCheckoutSessionPermissionOrThrow({
|
||||
workspaceId,
|
||||
userWorkspaceId,
|
||||
|
||||
@ -5,9 +5,9 @@ import { Field, ObjectType } from '@nestjs/graphql';
|
||||
import { BillingPriceLicensedDTO } from 'src/engine/core-modules/billing/dtos/billing-price-licensed.dto';
|
||||
import { BillingPriceMeteredDTO } from 'src/engine/core-modules/billing/dtos/billing-price-metered.dto';
|
||||
import { BillingPriceUnionDTO } from 'src/engine/core-modules/billing/dtos/billing-price-union.dto';
|
||||
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
||||
import { BillingProductMetadata } from 'src/engine/core-modules/billing/types/billing-product-metadata.type';
|
||||
|
||||
@ObjectType()
|
||||
@ObjectType('BillingProduct')
|
||||
export class BillingProductDTO {
|
||||
@Field(() => String)
|
||||
name: string;
|
||||
@ -18,9 +18,9 @@ export class BillingProductDTO {
|
||||
@Field(() => [String], { nullable: true })
|
||||
images: string[];
|
||||
|
||||
@Field(() => BillingUsageType)
|
||||
type: BillingUsageType;
|
||||
|
||||
@Field(() => [BillingPriceUnionDTO])
|
||||
@Field(() => [BillingPriceUnionDTO], { nullable: true })
|
||||
prices: Array<BillingPriceLicensedDTO> | Array<BillingPriceMeteredDTO>;
|
||||
|
||||
@Field(() => BillingProductMetadata)
|
||||
metadata: BillingProductMetadata;
|
||||
}
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
|
||||
|
||||
@ObjectType()
|
||||
export class BillingEndTrialPeriodOutput {
|
||||
@Field(() => SubscriptionStatus, {
|
||||
description: 'Updated subscription status',
|
||||
nullable: true,
|
||||
})
|
||||
status: SubscriptionStatus | undefined;
|
||||
|
||||
@Field(() => Boolean, {
|
||||
description: 'Boolean that confirms if a payment method was found',
|
||||
})
|
||||
hasPaymentMethod: boolean;
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { BillingProductDTO } from 'src/engine/core-modules/billing/dtos/billing-product.dto';
|
||||
|
||||
@ObjectType('BillingSubscriptionItem')
|
||||
export class BillingSubscriptionItemDTO {
|
||||
@IDField(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@Field(() => Boolean)
|
||||
hasReachedCurrentPeriodCap: boolean;
|
||||
|
||||
@Field(() => BillingProductDTO, { nullable: true })
|
||||
billingProduct: BillingProductDTO;
|
||||
}
|
||||
@ -5,6 +5,7 @@ import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
Relation,
|
||||
@ -12,6 +13,7 @@ import {
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { BillingProduct } from 'src/engine/core-modules/billing/entities/billing-product.entity';
|
||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
@Entity({ name: 'billingSubscriptionItem', schema: 'core' })
|
||||
@Unique('IndexOnBillingSubscriptionIdAndStripeProductIdUnique', [
|
||||
@ -52,6 +54,13 @@ export class BillingSubscriptionItem {
|
||||
)
|
||||
billingSubscription: Relation<BillingSubscription>;
|
||||
|
||||
@ManyToOne(() => BillingProduct)
|
||||
@JoinColumn({
|
||||
name: 'stripeProductId',
|
||||
referencedColumnName: 'stripeProductId',
|
||||
})
|
||||
billingProduct: Relation<BillingProduct>;
|
||||
|
||||
@Column({ nullable: false })
|
||||
stripeProductId: string;
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
} from 'typeorm';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { BillingSubscriptionItemDTO } from 'src/engine/core-modules/billing/dtos/outputs/billing-subscription-item.output';
|
||||
import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
|
||||
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
|
||||
import { BillingSubscriptionCollectionMethod } from 'src/engine/core-modules/billing/enums/billing-subscription-collection-method.enum';
|
||||
@ -72,6 +73,7 @@ export class BillingSubscription {
|
||||
})
|
||||
interval: Stripe.Price.Recurring.Interval;
|
||||
|
||||
@Field(() => [BillingSubscriptionItemDTO], { nullable: true })
|
||||
@OneToMany(
|
||||
() => BillingSubscriptionItem,
|
||||
(billingSubscriptionItem) => billingSubscriptionItem.billingSubscription,
|
||||
|
||||
@ -1,6 +1,13 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
export enum BillingProductKey {
|
||||
BASE_PRODUCT = 'BASE_PRODUCT',
|
||||
WORKFLOW_NODE_EXECUTION = 'WORKFLOW_NODE_EXECUTION',
|
||||
}
|
||||
|
||||
registerEnumType(BillingProductKey, {
|
||||
name: 'BillingProductKey',
|
||||
description: 'The different billing products available',
|
||||
});
|
||||
|
||||
@ -1,6 +1,13 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
export enum BillingUsageType {
|
||||
METERED = 'METERED',
|
||||
LICENSED = 'LICENSED',
|
||||
}
|
||||
|
||||
registerEnumType(BillingUsageType, {
|
||||
name: 'BillingUsageType',
|
||||
description: 'The different billing usage types',
|
||||
});
|
||||
|
||||
@ -48,6 +48,7 @@ export class BillingRestApiExceptionFilter implements ExceptionFilter {
|
||||
404,
|
||||
);
|
||||
case BillingExceptionCode.BILLING_METER_EVENT_FAILED:
|
||||
case BillingExceptionCode.BILLING_SUBSCRIPTION_NOT_IN_TRIAL_PERIOD:
|
||||
return this.httpExceptionHandlerService.handleError(
|
||||
exception,
|
||||
response,
|
||||
|
||||
@ -21,15 +21,15 @@ import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/bill
|
||||
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
|
||||
import { BillingPlanService } from 'src/engine/core-modules/billing/services/billing-plan.service';
|
||||
import { BillingProductService } from 'src/engine/core-modules/billing/services/billing-product.service';
|
||||
import { StripeSubscriptionItemService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service';
|
||||
import { StripeCustomerService } from 'src/engine/core-modules/billing/stripe/services/stripe-customer.service';
|
||||
import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service';
|
||||
import { getPlanKeyFromSubscription } from 'src/engine/core-modules/billing/utils/get-plan-key-from-subscription.util';
|
||||
import { getSubscriptionStatus } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-subscription.util';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@Injectable()
|
||||
export class BillingSubscriptionService {
|
||||
protected readonly logger = new Logger(BillingSubscriptionService.name);
|
||||
constructor(
|
||||
private readonly stripeSubscriptionItemService: StripeSubscriptionItemService,
|
||||
private readonly stripeSubscriptionService: StripeSubscriptionService,
|
||||
private readonly billingPlanService: BillingPlanService,
|
||||
private readonly billingProductService: BillingProductService,
|
||||
@ -37,6 +37,7 @@ export class BillingSubscriptionService {
|
||||
private readonly billingEntitlementRepository: Repository<BillingEntitlement>,
|
||||
@InjectRepository(BillingSubscription, 'core')
|
||||
private readonly billingSubscriptionRepository: Repository<BillingSubscription>,
|
||||
private readonly stripeCustomerService: StripeCustomerService,
|
||||
) {}
|
||||
|
||||
async getCurrentBillingSubscriptionOrThrow(criteria: {
|
||||
@ -46,7 +47,10 @@ export class BillingSubscriptionService {
|
||||
const notCanceledSubscriptions =
|
||||
await this.billingSubscriptionRepository.find({
|
||||
where: { ...criteria, status: Not(SubscriptionStatus.Canceled) },
|
||||
relations: ['billingSubscriptionItems'],
|
||||
relations: [
|
||||
'billingSubscriptionItems',
|
||||
'billingSubscriptionItems.billingProduct',
|
||||
],
|
||||
});
|
||||
|
||||
assert(
|
||||
@ -195,4 +199,38 @@ export class BillingSubscriptionService {
|
||||
|
||||
return subscriptionItemsToUpdate;
|
||||
}
|
||||
|
||||
async endTrialPeriod(workspace: Workspace) {
|
||||
const billingSubscription = await this.getCurrentBillingSubscriptionOrThrow(
|
||||
{ workspaceId: workspace.id },
|
||||
);
|
||||
|
||||
if (billingSubscription.status !== SubscriptionStatus.Trialing) {
|
||||
throw new BillingException(
|
||||
'Billing subscription is not in trial period',
|
||||
BillingExceptionCode.BILLING_SUBSCRIPTION_NOT_IN_TRIAL_PERIOD,
|
||||
);
|
||||
}
|
||||
|
||||
const hasPaymentMethod = await this.stripeCustomerService.hasPaymentMethod(
|
||||
billingSubscription.stripeCustomerId,
|
||||
);
|
||||
|
||||
if (!hasPaymentMethod) {
|
||||
return { hasPaymentMethod: false, status: undefined };
|
||||
}
|
||||
|
||||
const updatedSubscription =
|
||||
await this.stripeSubscriptionService.updateSubscription(
|
||||
billingSubscription.stripeSubscriptionId,
|
||||
{
|
||||
trial_end: 'now',
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
status: getSubscriptionStatus(updatedSubscription.status),
|
||||
hasPaymentMethod: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,4 +32,11 @@ export class StripeCustomerService {
|
||||
metadata: { workspaceId: workspaceId },
|
||||
});
|
||||
}
|
||||
|
||||
async hasPaymentMethod(stripeCustomerId: string) {
|
||||
const { data: paymentMethods } =
|
||||
await this.stripe.customers.listPaymentMethods(stripeCustomerId);
|
||||
|
||||
return paymentMethods.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,4 +76,11 @@ export class StripeSubscriptionService {
|
||||
items: stripeSubscriptionItemsToUpdate,
|
||||
});
|
||||
}
|
||||
|
||||
async updateSubscription(
|
||||
stripeSubscriptionId: string,
|
||||
updateData: Stripe.SubscriptionUpdateParams,
|
||||
): Promise<Stripe.Subscription> {
|
||||
return this.stripe.subscriptions.update(stripeSubscriptionId, updateData);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,21 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
|
||||
import { BillingProductKey } from 'src/engine/core-modules/billing/enums/billing-product-key.enum';
|
||||
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
||||
|
||||
export type BillingProductMetadata =
|
||||
| {
|
||||
planKey: BillingPlanKey;
|
||||
priceUsageBased: BillingUsageType;
|
||||
productKey: BillingProductKey;
|
||||
[key: string]: string;
|
||||
}
|
||||
| Record<string, never>;
|
||||
@ObjectType('BillingProductMetadata')
|
||||
export class BillingProductMetadata {
|
||||
@Field(() => BillingPlanKey)
|
||||
planKey: BillingPlanKey;
|
||||
|
||||
@Field(() => BillingUsageType)
|
||||
priceUsageBased: BillingUsageType;
|
||||
|
||||
@Field(() => BillingProductKey)
|
||||
productKey: BillingProductKey;
|
||||
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
@ -68,6 +68,9 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => {
|
||||
planKey: BillingPlanKey.PRO,
|
||||
baseProduct: {
|
||||
id: 'base-1',
|
||||
metadata: {
|
||||
priceUsageBased: BillingUsageType.LICENSED,
|
||||
},
|
||||
name: 'Base Product',
|
||||
billingPrices: [
|
||||
{
|
||||
@ -77,7 +80,6 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => {
|
||||
priceUsageType: BillingUsageType.LICENSED,
|
||||
},
|
||||
],
|
||||
type: BillingUsageType.LICENSED,
|
||||
prices: [
|
||||
{
|
||||
recurringInterval: SubscriptionInterval.Month,
|
||||
@ -90,6 +92,9 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => {
|
||||
otherLicensedProducts: [
|
||||
{
|
||||
id: 'licensed-1',
|
||||
metadata: {
|
||||
priceUsageBased: BillingUsageType.LICENSED,
|
||||
},
|
||||
name: 'Licensed Product',
|
||||
billingPrices: [
|
||||
{
|
||||
@ -99,7 +104,6 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => {
|
||||
priceUsageType: BillingUsageType.LICENSED,
|
||||
},
|
||||
],
|
||||
type: BillingUsageType.LICENSED,
|
||||
prices: [
|
||||
{
|
||||
recurringInterval: SubscriptionInterval.Year,
|
||||
@ -113,6 +117,9 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => {
|
||||
meteredProducts: [
|
||||
{
|
||||
id: 'metered-1',
|
||||
metadata: {
|
||||
priceUsageBased: BillingUsageType.METERED,
|
||||
},
|
||||
name: 'Metered Product',
|
||||
billingPrices: [
|
||||
{
|
||||
@ -129,7 +136,6 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => {
|
||||
priceUsageType: BillingUsageType.METERED,
|
||||
},
|
||||
],
|
||||
type: BillingUsageType.METERED,
|
||||
prices: [
|
||||
{
|
||||
tiersMode: BillingPriceTiersMode.GRADUATED,
|
||||
@ -191,6 +197,9 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => {
|
||||
planKey: 'empty-plan',
|
||||
baseProduct: {
|
||||
id: 'base-1',
|
||||
metadata: {
|
||||
priceUsageBased: BillingUsageType.LICENSED,
|
||||
},
|
||||
name: 'Base Product',
|
||||
billingPrices: [
|
||||
{
|
||||
@ -200,7 +209,6 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => {
|
||||
priceUsageType: BillingUsageType.LICENSED,
|
||||
},
|
||||
],
|
||||
type: BillingUsageType.LICENSED,
|
||||
prices: [
|
||||
{
|
||||
recurringInterval: SubscriptionInterval.Month,
|
||||
@ -214,6 +222,9 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => {
|
||||
meteredProducts: [
|
||||
{
|
||||
id: 'metered-1',
|
||||
metadata: {
|
||||
priceUsageBased: BillingUsageType.METERED,
|
||||
},
|
||||
name: 'Metered Product',
|
||||
billingPrices: [
|
||||
{
|
||||
@ -224,7 +235,6 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => {
|
||||
priceUsageType: BillingUsageType.METERED,
|
||||
},
|
||||
],
|
||||
type: BillingUsageType.METERED,
|
||||
prices: [
|
||||
{
|
||||
tiersMode: null,
|
||||
|
||||
@ -16,7 +16,10 @@ export const formatBillingDatabaseProductToGraphqlDTO = (
|
||||
planKey: plan.planKey,
|
||||
baseProduct: {
|
||||
...plan.baseProduct,
|
||||
type: BillingUsageType.LICENSED,
|
||||
metadata: {
|
||||
...plan.baseProduct.metadata,
|
||||
priceUsageBased: BillingUsageType.LICENSED,
|
||||
},
|
||||
prices: plan.baseProduct.billingPrices.map(
|
||||
formatBillingDatabasePriceToLicensedPriceDTO,
|
||||
),
|
||||
@ -24,7 +27,10 @@ export const formatBillingDatabaseProductToGraphqlDTO = (
|
||||
otherLicensedProducts: plan.otherLicensedProducts.map((product) => {
|
||||
return {
|
||||
...product,
|
||||
type: BillingUsageType.LICENSED,
|
||||
metadata: {
|
||||
...product.metadata,
|
||||
priceUsageBased: BillingUsageType.LICENSED,
|
||||
},
|
||||
prices: product.billingPrices.map(
|
||||
formatBillingDatabasePriceToLicensedPriceDTO,
|
||||
),
|
||||
@ -33,7 +39,10 @@ export const formatBillingDatabaseProductToGraphqlDTO = (
|
||||
meteredProducts: plan.meteredProducts.map((product) => {
|
||||
return {
|
||||
...product,
|
||||
type: BillingUsageType.METERED,
|
||||
metadata: {
|
||||
...product.metadata,
|
||||
priceUsageBased: BillingUsageType.METERED,
|
||||
},
|
||||
prices: product.billingPrices.map(
|
||||
formatBillingDatabasePriceToMeteredPriceDTO,
|
||||
),
|
||||
|
||||
@ -7,9 +7,6 @@ import Stripe from 'stripe';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { BillingProduct } from 'src/engine/core-modules/billing/entities/billing-product.entity';
|
||||
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
|
||||
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
||||
import { BillingProductMetadata } from 'src/engine/core-modules/billing/types/billing-product-metadata.type';
|
||||
import { isStripeValidProductMetadata } from 'src/engine/core-modules/billing/utils/is-stripe-valid-product-metadata.util';
|
||||
import { transformStripeProductEventToDatabaseProduct } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-product-event-to-database-product.util';
|
||||
@Injectable()
|
||||
@ -40,28 +37,4 @@ export class BillingWebhookProductService {
|
||||
stripeProductId: data.object.id,
|
||||
};
|
||||
}
|
||||
|
||||
isStripeValidProductMetadata(
|
||||
metadata: Stripe.Metadata,
|
||||
): metadata is BillingProductMetadata {
|
||||
if (Object.keys(metadata).length === 0) {
|
||||
return true;
|
||||
}
|
||||
const hasBillingPlanKey = this.isValidBillingPlanKey(metadata?.planKey);
|
||||
const hasPriceUsageBased = this.isValidPriceUsageBased(
|
||||
metadata?.priceUsageBased,
|
||||
);
|
||||
|
||||
return hasBillingPlanKey && hasPriceUsageBased;
|
||||
}
|
||||
|
||||
isValidBillingPlanKey(planKey?: string) {
|
||||
return Object.values(BillingPlanKey).includes(planKey as BillingPlanKey);
|
||||
}
|
||||
|
||||
isValidPriceUsageBased(priceUsageBased?: string) {
|
||||
return Object.values(BillingUsageType).includes(
|
||||
priceUsageBased as BillingUsageType,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,7 +53,7 @@ export const transformStripeSubscriptionEventToDatabaseSubscription = (
|
||||
};
|
||||
};
|
||||
|
||||
const getSubscriptionStatus = (status: Stripe.Subscription.Status) => {
|
||||
export const getSubscriptionStatus = (status: Stripe.Subscription.Status) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return SubscriptionStatus.Active;
|
||||
|
||||
Reference in New Issue
Block a user