eliminate IS_BILLING_PLANS_ENABLED feature flag (#10678)
Solves: https://github.com/twentyhq/core-team-issues/issues/527 **TLDR:** Basically the title. Fetches the product and prices from the database instead of the environment variables. **What this means:** - new subscriptions in twenty will be hybrid (per seat subscription plus an usage base product) - right now the price for the usage base product is 0$ per unit - The existing subscription will work normally, however we will need to update their subscription items in order to contain the usage base product (remember that the pricing intervals like monthly or yearly should match in all the subscription items) - The previous point can be done using Stripe Postman **In order to test:** - Have the environment variable IS_BILLING_ENABLED set to true and add the other required environment variables for Billing to work - Do a database reset (to ensure that the new feature flag is deleted and that the billing tables are created) - Run the command: npx nx run twenty-server:command billing:sync-plans-data (if you don't do that the products and prices will not be present in the database) - Run the server , the frontend, the worker, and the stripe listen command (stripe listen --forward-to http://localhost:3000/billing/webhooks) - Buy a subscription for acme workspace - Update the quantity of members in a workspace (add or delete) - Change the subscription interval --------- Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
90d01a6c58
commit
4be75fb7da
@ -170,6 +170,7 @@ export type BillingPlanOutput = {
|
||||
|
||||
export type BillingPriceLicensedDto = {
|
||||
__typename?: 'BillingPriceLicensedDTO';
|
||||
priceUsageType: BillingUsageType;
|
||||
recurringInterval: SubscriptionInterval;
|
||||
stripePriceId: Scalars['String']['output'];
|
||||
unitAmount: Scalars['Float']['output'];
|
||||
@ -177,6 +178,7 @@ export type BillingPriceLicensedDto = {
|
||||
|
||||
export type BillingPriceMeteredDto = {
|
||||
__typename?: 'BillingPriceMeteredDTO';
|
||||
priceUsageType: BillingUsageType;
|
||||
recurringInterval: SubscriptionInterval;
|
||||
stripePriceId: Scalars['String']['output'];
|
||||
tiers?: Maybe<Array<BillingPriceTierDto>>;
|
||||
@ -203,24 +205,10 @@ export type BillingProductDto = {
|
||||
description: Scalars['String']['output'];
|
||||
images?: Maybe<Array<Scalars['String']['output']>>;
|
||||
name: Scalars['String']['output'];
|
||||
prices: Array<Maybe<BillingPriceUnionDto>>;
|
||||
prices: Array<BillingPriceUnionDto>;
|
||||
type: BillingUsageType;
|
||||
};
|
||||
|
||||
export type BillingProductPriceDto = {
|
||||
__typename?: 'BillingProductPriceDTO';
|
||||
created: Scalars['Float']['output'];
|
||||
recurringInterval: SubscriptionInterval;
|
||||
stripePriceId: Scalars['String']['output'];
|
||||
unitAmount: Scalars['Float']['output'];
|
||||
};
|
||||
|
||||
export type BillingProductPricesOutput = {
|
||||
__typename?: 'BillingProductPricesOutput';
|
||||
productPrices: Array<BillingProductPriceDto>;
|
||||
totalNumberOfPrices: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type BillingSessionOutput = {
|
||||
__typename?: 'BillingSessionOutput';
|
||||
url?: Maybe<Scalars['String']['output']>;
|
||||
@ -560,7 +548,6 @@ export enum FeatureFlagKey {
|
||||
IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled',
|
||||
IsAnalyticsV2Enabled = 'IsAnalyticsV2Enabled',
|
||||
IsApprovedAccessDomainsEnabled = 'IsApprovedAccessDomainsEnabled',
|
||||
IsBillingPlansEnabled = 'IsBillingPlansEnabled',
|
||||
IsCommandMenuV2Enabled = 'IsCommandMenuV2Enabled',
|
||||
IsCopilotEnabled = 'IsCopilotEnabled',
|
||||
IsCustomDomainEnabled = 'IsCustomDomainEnabled',
|
||||
@ -1444,7 +1431,6 @@ export type Query = {
|
||||
getEnvironmentVariablesGrouped: EnvironmentVariablesOutput;
|
||||
getIndicatorHealthStatus: AdminPanelHealthServiceData;
|
||||
getPostgresCredentials?: Maybe<PostgresCredentials>;
|
||||
getProductPrices: BillingProductPricesOutput;
|
||||
getPublicWorkspaceDataByDomain: PublicWorkspaceDataOutput;
|
||||
getRoles: Array<Role>;
|
||||
getSSOIdentityProviders: Array<FindAvailableSsoidpOutput>;
|
||||
@ -1527,10 +1513,6 @@ export type QueryGetIndicatorHealthStatusArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryGetProductPricesArgs = {
|
||||
product: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryGetServerlessFunctionSourceCodeArgs = {
|
||||
input: GetServerlessFunctionSourceCodeInput;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import * as Apollo from '@apollo/client';
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
export type Maybe<T> = T | null;
|
||||
export type InputMaybe<T> = Maybe<T>;
|
||||
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
||||
@ -491,7 +491,6 @@ export enum FeatureFlagKey {
|
||||
IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled',
|
||||
IsAnalyticsV2Enabled = 'IsAnalyticsV2Enabled',
|
||||
IsApprovedAccessDomainsEnabled = 'IsApprovedAccessDomainsEnabled',
|
||||
IsBillingPlansEnabled = 'IsBillingPlansEnabled',
|
||||
IsCommandMenuV2Enabled = 'IsCommandMenuV2Enabled',
|
||||
IsCopilotEnabled = 'IsCopilotEnabled',
|
||||
IsCustomDomainEnabled = 'IsCustomDomainEnabled',
|
||||
@ -1390,11 +1389,6 @@ export type QueryGetIndicatorHealthStatusArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryGetProductPricesArgs = {
|
||||
product: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryGetQueueMetricsArgs = {
|
||||
queueName: Scalars['String'];
|
||||
timeRange?: InputMaybe<QueueMetricsTimeRange>;
|
||||
|
||||
@ -55,11 +55,6 @@ export const seedFeatureFlags = async (
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IsBillingPlansEnabled,
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IsUniqueIndexesEnabled,
|
||||
workspaceId: workspaceId,
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
import { UseFilters, UseGuards } from '@nestjs/common';
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { GraphQLError } from 'graphql';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
|
||||
import { BillingCheckoutSessionInput } from 'src/engine/core-modules/billing/dtos/inputs/billing-checkout-session.input';
|
||||
@ -11,13 +10,11 @@ import { BillingSessionInput } from 'src/engine/core-modules/billing/dtos/inputs
|
||||
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';
|
||||
import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum';
|
||||
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
|
||||
import { BillingPlanService } from 'src/engine/core-modules/billing/services/billing-plan.service';
|
||||
import { BillingPortalWorkspaceService } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service';
|
||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
||||
import { StripePriceService } from 'src/engine/core-modules/billing/stripe/services/stripe-price.service';
|
||||
import { BillingPortalCheckoutSessionParameters } from 'src/engine/core-modules/billing/types/billing-portal-checkout-session-parameters.type';
|
||||
import { formatBillingDatabaseProductToGraphqlDTO } from 'src/engine/core-modules/billing/utils/format-database-product-to-graphql-dto.util';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
@ -47,7 +44,6 @@ export class BillingResolver {
|
||||
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||
private readonly billingPortalWorkspaceService: BillingPortalWorkspaceService,
|
||||
private readonly billingPlanService: BillingPlanService,
|
||||
private readonly stripePriceService: StripePriceService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly billingService: BillingService,
|
||||
private readonly permissionsService: PermissionsService,
|
||||
@ -90,11 +86,6 @@ export class BillingResolver {
|
||||
userWorkspaceId,
|
||||
isExecutedByApiKey: isDefined(apiKey),
|
||||
});
|
||||
const isBillingPlansEnabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IsBillingPlansEnabled,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
const checkoutSessionParams: BillingPortalCheckoutSessionParameters = {
|
||||
user,
|
||||
@ -104,37 +95,16 @@ export class BillingResolver {
|
||||
requirePaymentMethod,
|
||||
};
|
||||
|
||||
if (isBillingPlansEnabled) {
|
||||
const billingPricesPerPlan =
|
||||
await this.billingPlanService.getPricesPerPlan({
|
||||
planKey: checkoutSessionParams.plan,
|
||||
interval: recurringInterval,
|
||||
});
|
||||
const checkoutSessionURL =
|
||||
await this.billingPortalWorkspaceService.computeCheckoutSessionURL({
|
||||
...checkoutSessionParams,
|
||||
billingPricesPerPlan,
|
||||
});
|
||||
|
||||
return {
|
||||
url: checkoutSessionURL,
|
||||
};
|
||||
}
|
||||
|
||||
const productPrice = await this.stripePriceService.getStripePrice(
|
||||
AvailableProduct.BasePlan,
|
||||
recurringInterval,
|
||||
const billingPricesPerPlan = await this.billingPlanService.getPricesPerPlan(
|
||||
{
|
||||
planKey: checkoutSessionParams.plan,
|
||||
interval: recurringInterval,
|
||||
},
|
||||
);
|
||||
|
||||
if (!productPrice) {
|
||||
throw new GraphQLError(
|
||||
'Product price not found for the given recurring interval',
|
||||
);
|
||||
}
|
||||
const checkoutSessionURL =
|
||||
await this.billingPortalWorkspaceService.computeCheckoutSessionURL({
|
||||
...checkoutSessionParams,
|
||||
priceId: productPrice.stripePriceId,
|
||||
billingPricesPerPlan,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
/* @license Enterprise */
|
||||
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
||||
@ObjectType()
|
||||
export class BillingProductPriceDTO {
|
||||
@Field(() => SubscriptionInterval)
|
||||
recurringInterval: Stripe.Price.Recurring.Interval;
|
||||
|
||||
@Field(() => Number)
|
||||
unitAmount: number;
|
||||
|
||||
@Field(() => Number)
|
||||
created: number;
|
||||
|
||||
@Field(() => String)
|
||||
stripePriceId: string;
|
||||
}
|
||||
@ -17,8 +17,6 @@ import { StripeCheckoutService } from 'src/engine/core-modules/billing/stripe/se
|
||||
import { BillingGetPricesPerPlanResult } from 'src/engine/core-modules/billing/types/billing-get-prices-per-plan-result.type';
|
||||
import { BillingPortalCheckoutSessionParameters } from 'src/engine/core-modules/billing/types/billing-portal-checkout-session-parameters.type';
|
||||
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { assert } from 'src/utils/assert';
|
||||
@ -30,7 +28,6 @@ export class BillingPortalWorkspaceService {
|
||||
private readonly stripeCheckoutService: StripeCheckoutService,
|
||||
private readonly stripeBillingPortalService: StripeBillingPortalService,
|
||||
private readonly domainManagerService: DomainManagerService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
@InjectRepository(BillingSubscription, 'core')
|
||||
private readonly billingSubscriptionRepository: Repository<BillingSubscription>,
|
||||
@InjectRepository(UserWorkspace, 'core')
|
||||
@ -43,7 +40,6 @@ export class BillingPortalWorkspaceService {
|
||||
billingPricesPerPlan,
|
||||
successUrlPath,
|
||||
plan,
|
||||
priceId,
|
||||
requirePaymentMethod,
|
||||
}: BillingPortalCheckoutSessionParameters): Promise<string> {
|
||||
const frontBaseUrl = this.domainManagerService.buildWorkspaceURL({
|
||||
@ -65,18 +61,11 @@ export class BillingPortalWorkspaceService {
|
||||
});
|
||||
|
||||
const stripeCustomerId = subscription?.stripeCustomerId;
|
||||
const isBillingPlansEnabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IsBillingPlansEnabled,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
const stripeSubscriptionLineItems =
|
||||
await this.getStripeSubscriptionLineItems({
|
||||
quantity,
|
||||
isBillingPlansEnabled,
|
||||
billingPricesPerPlan,
|
||||
priceId,
|
||||
});
|
||||
|
||||
const checkoutSession =
|
||||
@ -90,7 +79,6 @@ export class BillingPortalWorkspaceService {
|
||||
plan,
|
||||
requirePaymentMethod,
|
||||
withTrialPeriod: !isDefined(subscription),
|
||||
isBillingPlansEnabled,
|
||||
});
|
||||
|
||||
assert(checkoutSession.url, 'Error: missing checkout.session.url');
|
||||
@ -139,16 +127,12 @@ export class BillingPortalWorkspaceService {
|
||||
|
||||
private getStripeSubscriptionLineItems({
|
||||
quantity,
|
||||
isBillingPlansEnabled,
|
||||
billingPricesPerPlan,
|
||||
priceId,
|
||||
}: {
|
||||
quantity: number;
|
||||
isBillingPlansEnabled: boolean;
|
||||
billingPricesPerPlan?: BillingGetPricesPerPlanResult;
|
||||
priceId?: string;
|
||||
}): Stripe.Checkout.SessionCreateParams.LineItem[] {
|
||||
if (isBillingPlansEnabled && billingPricesPerPlan) {
|
||||
if (billingPricesPerPlan) {
|
||||
return [
|
||||
{
|
||||
price: billingPricesPerPlan.baseProductPrice.stripePriceId,
|
||||
@ -160,14 +144,8 @@ export class BillingPortalWorkspaceService {
|
||||
];
|
||||
}
|
||||
|
||||
if (priceId && !isBillingPlansEnabled) {
|
||||
return [{ price: priceId, quantity }];
|
||||
}
|
||||
|
||||
throw new BillingException(
|
||||
isBillingPlansEnabled
|
||||
? 'Missing Billing prices per plan'
|
||||
: 'Missing price id',
|
||||
'Missing Billing prices per plan',
|
||||
BillingExceptionCode.BILLING_PRICE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
@ -16,31 +16,21 @@ import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/bil
|
||||
import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-price.entity';
|
||||
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
|
||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum';
|
||||
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
|
||||
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
|
||||
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
||||
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 { StripePriceService } from 'src/engine/core-modules/billing/stripe/services/stripe-price.service';
|
||||
import { StripeSubscriptionItemService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-item.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 { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@Injectable()
|
||||
export class BillingSubscriptionService {
|
||||
protected readonly logger = new Logger(BillingSubscriptionService.name);
|
||||
constructor(
|
||||
private readonly stripeSubscriptionService: StripeSubscriptionService,
|
||||
private readonly stripePriceService: StripePriceService,
|
||||
private readonly stripeSubscriptionItemService: StripeSubscriptionItemService,
|
||||
private readonly billingPlanService: BillingPlanService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly billingProductService: BillingProductService,
|
||||
@InjectRepository(BillingEntitlement, 'core')
|
||||
private readonly billingEntitlementRepository: Repository<BillingEntitlement>,
|
||||
@ -68,24 +58,14 @@ export class BillingSubscriptionService {
|
||||
|
||||
async getBaseProductCurrentBillingSubscriptionItemOrThrow(
|
||||
workspaceId: string,
|
||||
stripeBaseProductId = this.environmentService.get(
|
||||
'BILLING_STRIPE_BASE_PLAN_PRODUCT_ID',
|
||||
),
|
||||
) {
|
||||
const isBillingPlansEnabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IsBillingPlansEnabled,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const billingSubscription = await this.getCurrentBillingSubscriptionOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const getStripeProductId = isBillingPlansEnabled
|
||||
? (await this.billingPlanService.getPlanBaseProduct(BillingPlanKey.PRO))
|
||||
?.stripeProductId
|
||||
: stripeBaseProductId;
|
||||
const getStripeProductId = (
|
||||
await this.billingPlanService.getPlanBaseProduct(BillingPlanKey.PRO)
|
||||
)?.stripeProductId;
|
||||
|
||||
if (!getStripeProductId) {
|
||||
throw new BillingException(
|
||||
@ -136,7 +116,7 @@ export class BillingSubscriptionService {
|
||||
|
||||
return {
|
||||
handleUnpaidInvoiceStripeSubscriptionId:
|
||||
billingSubscription.stripeSubscriptionId,
|
||||
billingSubscription?.stripeSubscriptionId,
|
||||
};
|
||||
}
|
||||
|
||||
@ -161,57 +141,29 @@ export class BillingSubscriptionService {
|
||||
const billingSubscription = await this.getCurrentBillingSubscriptionOrThrow(
|
||||
{ workspaceId: workspace.id },
|
||||
);
|
||||
const isBillingPlansEnabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IsBillingPlansEnabled,
|
||||
workspace.id,
|
||||
);
|
||||
const newInterval =
|
||||
billingSubscription?.interval === SubscriptionInterval.Year
|
||||
? SubscriptionInterval.Month
|
||||
: SubscriptionInterval.Year;
|
||||
|
||||
const billingBaseProductSubscriptionItem =
|
||||
await this.getBaseProductCurrentBillingSubscriptionItemOrThrow(
|
||||
workspace.id,
|
||||
);
|
||||
const planKey = getPlanKeyFromSubscription(billingSubscription);
|
||||
const billingProductsByPlan =
|
||||
await this.billingProductService.getProductsByPlan(planKey);
|
||||
const pricesPerPlanArray =
|
||||
this.billingProductService.getProductPricesByInterval({
|
||||
interval: newInterval,
|
||||
billingProductsByPlan,
|
||||
});
|
||||
|
||||
if (isBillingPlansEnabled) {
|
||||
const planKey = getPlanKeyFromSubscription(billingSubscription);
|
||||
const billingProductsByPlan =
|
||||
await this.billingProductService.getProductsByPlan(planKey);
|
||||
const pricesPerPlanArray =
|
||||
this.billingProductService.getProductPricesByInterval({
|
||||
interval: newInterval,
|
||||
billingProductsByPlan,
|
||||
});
|
||||
const subscriptionItemsToUpdate = this.getSubscriptionItemsToUpdate(
|
||||
billingSubscription,
|
||||
pricesPerPlanArray,
|
||||
);
|
||||
|
||||
const subscriptionItemsToUpdate = this.getSubscriptionItemsToUpdate(
|
||||
billingSubscription,
|
||||
pricesPerPlanArray,
|
||||
);
|
||||
|
||||
await this.stripeSubscriptionService.updateSubscriptionItems(
|
||||
billingSubscription.stripeSubscriptionId,
|
||||
subscriptionItemsToUpdate,
|
||||
);
|
||||
} else {
|
||||
const productPrice = await this.stripePriceService.getStripePrice(
|
||||
AvailableProduct.BasePlan,
|
||||
newInterval,
|
||||
);
|
||||
|
||||
if (!productPrice) {
|
||||
throw new Error(
|
||||
`Cannot find product price for product ${AvailableProduct.BasePlan} and interval ${newInterval}`,
|
||||
);
|
||||
}
|
||||
|
||||
await this.stripeSubscriptionItemService.updateBillingSubscriptionItem(
|
||||
billingBaseProductSubscriptionItem,
|
||||
productPrice.stripePriceId,
|
||||
);
|
||||
}
|
||||
await this.stripeSubscriptionService.updateSubscriptionItems(
|
||||
billingSubscription.stripeSubscriptionId,
|
||||
subscriptionItemsToUpdate,
|
||||
);
|
||||
}
|
||||
|
||||
private getSubscriptionItemsToUpdate(
|
||||
|
||||
@ -13,9 +13,6 @@ import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billin
|
||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||
import { StripeBillingMeterEventService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-meter-event.service';
|
||||
import { BillingUsageEvent } from 'src/engine/core-modules/billing/types/billing-usage-event.type';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
|
||||
@Injectable()
|
||||
export class BillingUsageService {
|
||||
@ -23,24 +20,11 @@ export class BillingUsageService {
|
||||
constructor(
|
||||
@InjectRepository(BillingCustomer, 'core')
|
||||
private readonly billingCustomerRepository: Repository<BillingCustomer>,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly stripeBillingMeterEventService: StripeBillingMeterEventService,
|
||||
) {}
|
||||
|
||||
async canFeatureBeUsed(workspaceId: string): Promise<boolean> {
|
||||
const isBillingEnabled = this.environmentService.get('IS_BILLING_ENABLED');
|
||||
const isBillingPlansEnabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IsBillingPlansEnabled,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (!isBillingPlansEnabled || !isBillingEnabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const billingSubscription =
|
||||
await this.billingSubscriptionService.getCurrentBillingSubscriptionOrThrow(
|
||||
{
|
||||
|
||||
@ -36,7 +36,6 @@ export class StripeCheckoutService {
|
||||
plan = BillingPlanKey.PRO,
|
||||
requirePaymentMethod = true,
|
||||
withTrialPeriod,
|
||||
isBillingPlansEnabled = false,
|
||||
}: {
|
||||
user: User;
|
||||
workspaceId: string;
|
||||
@ -47,7 +46,6 @@ export class StripeCheckoutService {
|
||||
plan?: BillingPlanKey;
|
||||
requirePaymentMethod?: boolean;
|
||||
withTrialPeriod: boolean;
|
||||
isBillingPlansEnabled: boolean;
|
||||
}): Promise<Stripe.Checkout.Session> {
|
||||
return await this.stripe.checkout.sessions.create({
|
||||
line_items: stripeSubscriptionLineItems,
|
||||
@ -66,9 +64,7 @@ export class StripeCheckoutService {
|
||||
),
|
||||
trial_settings: {
|
||||
end_behavior: {
|
||||
missing_payment_method: isBillingPlansEnabled
|
||||
? 'create_invoice'
|
||||
: 'pause',
|
||||
missing_payment_method: 'pause',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -4,8 +4,6 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { BillingProductPriceDTO } from 'src/engine/core-modules/billing/dtos/billing-product-price.dto';
|
||||
import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum';
|
||||
import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
@ -26,57 +24,6 @@ export class StripePriceService {
|
||||
);
|
||||
}
|
||||
|
||||
async getStripePrices(product: AvailableProduct) {
|
||||
const stripeProductId = this.getStripeProductId(product);
|
||||
|
||||
const prices = await this.stripe.prices.search({
|
||||
query: `product: '${stripeProductId}'`,
|
||||
});
|
||||
|
||||
return this.formatProductPrices(prices.data);
|
||||
}
|
||||
|
||||
async getStripePrice(product: AvailableProduct, recurringInterval: string) {
|
||||
const productPrices = await this.getStripePrices(product);
|
||||
|
||||
return productPrices.find(
|
||||
(price) => price.recurringInterval === recurringInterval,
|
||||
);
|
||||
}
|
||||
|
||||
getStripeProductId(product: AvailableProduct) {
|
||||
if (product === AvailableProduct.BasePlan) {
|
||||
return this.environmentService.get('BILLING_STRIPE_BASE_PLAN_PRODUCT_ID');
|
||||
}
|
||||
}
|
||||
|
||||
formatProductPrices(prices: Stripe.Price[]): BillingProductPriceDTO[] {
|
||||
const productPrices: BillingProductPriceDTO[] = Object.values(
|
||||
prices
|
||||
.filter((item) => item.recurring?.interval && item.unit_amount)
|
||||
.reduce((acc, item: Stripe.Price) => {
|
||||
const interval = item.recurring?.interval;
|
||||
|
||||
if (!interval || !item.unit_amount) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (!acc[interval] || item.created > acc[interval].created) {
|
||||
acc[interval] = {
|
||||
unitAmount: item.unit_amount,
|
||||
recurringInterval: interval,
|
||||
created: item.created,
|
||||
stripePriceId: item.id,
|
||||
};
|
||||
}
|
||||
|
||||
return acc satisfies Record<string, BillingProductPriceDTO>;
|
||||
}, {}),
|
||||
);
|
||||
|
||||
return productPrices.sort((a, b) => a.unitAmount - b.unitAmount);
|
||||
}
|
||||
|
||||
async getPricesByProductId(productId: string) {
|
||||
const prices = await this.stripe.prices.list({
|
||||
product: productId,
|
||||
|
||||
@ -4,7 +4,6 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity';
|
||||
import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
|
||||
@ -28,20 +27,4 @@ export class StripeSubscriptionItemService {
|
||||
async updateSubscriptionItem(stripeItemId: string, quantity: number) {
|
||||
await this.stripe.subscriptionItems.update(stripeItemId, { quantity });
|
||||
}
|
||||
|
||||
async updateBillingSubscriptionItem(
|
||||
stripeSubscriptionItem: BillingSubscriptionItem,
|
||||
stripePriceId: string,
|
||||
) {
|
||||
await this.stripe.subscriptionItems.update(
|
||||
stripeSubscriptionItem.stripeSubscriptionItemId,
|
||||
{
|
||||
price: stripePriceId,
|
||||
quantity:
|
||||
stripeSubscriptionItem.quantity === null
|
||||
? undefined
|
||||
: stripeSubscriptionItem.quantity,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,5 @@ export type BillingPortalCheckoutSessionParameters = {
|
||||
billingPricesPerPlan?: BillingGetPricesPerPlanResult;
|
||||
successUrlPath?: string;
|
||||
plan: BillingPlanKey;
|
||||
priceId?: string;
|
||||
requirePaymentMethod?: boolean;
|
||||
};
|
||||
|
||||
@ -12,7 +12,6 @@ export enum FeatureFlagKey {
|
||||
IsJsonFilterEnabled = 'IS_JSON_FILTER_ENABLED',
|
||||
IsCustomDomainEnabled = 'IS_CUSTOM_DOMAIN_ENABLED',
|
||||
IsApprovedAccessDomainsEnabled = 'IS_APPROVED_ACCESS_DOMAINS_ENABLED',
|
||||
IsBillingPlansEnabled = 'IS_BILLING_PLANS_ENABLED',
|
||||
IsNewRelationEnabled = 'IS_NEW_RELATION_ENABLED',
|
||||
IsPermissionsEnabled = 'IS_PERMISSIONS_ENABLED',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user