deprocate getProductPrices query in front end (#10397)

**TLDR:**
Deprecate getProductPrices in the frontEnd and replace it with
BillingBaseProductPrices.

**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 properly
added 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 the choose your plan should be
using the new front end endpoint
This commit is contained in:
Ana Sofia Marin Alexandre
2025-03-05 11:27:34 -03:00
committed by GitHub
parent e838dfc68b
commit 4d7e52ef25
17 changed files with 174 additions and 151 deletions

View File

@ -1,5 +1,5 @@
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client'; import * as Apollo from '@apollo/client';
import { gql } from '@apollo/client';
export type Maybe<T> = T | null; export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>; export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] }; export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
@ -161,6 +161,7 @@ export type BillingPlanOutput = {
export type BillingPriceLicensedDto = { export type BillingPriceLicensedDto = {
__typename?: 'BillingPriceLicensedDTO'; __typename?: 'BillingPriceLicensedDTO';
priceUsageType: BillingUsageType;
recurringInterval: SubscriptionInterval; recurringInterval: SubscriptionInterval;
stripePriceId: Scalars['String']; stripePriceId: Scalars['String'];
unitAmount: Scalars['Float']; unitAmount: Scalars['Float'];
@ -168,6 +169,7 @@ export type BillingPriceLicensedDto = {
export type BillingPriceMeteredDto = { export type BillingPriceMeteredDto = {
__typename?: 'BillingPriceMeteredDTO'; __typename?: 'BillingPriceMeteredDTO';
priceUsageType: BillingUsageType;
recurringInterval: SubscriptionInterval; recurringInterval: SubscriptionInterval;
stripePriceId: Scalars['String']; stripePriceId: Scalars['String'];
tiers?: Maybe<Array<BillingPriceTierDto>>; tiers?: Maybe<Array<BillingPriceTierDto>>;
@ -194,24 +196,10 @@ export type BillingProductDto = {
description: Scalars['String']; description: Scalars['String'];
images?: Maybe<Array<Scalars['String']>>; images?: Maybe<Array<Scalars['String']>>;
name: Scalars['String']; name: Scalars['String'];
prices: Array<Maybe<BillingPriceUnionDto>>; prices: Array<BillingPriceUnionDto>;
type: BillingUsageType; type: BillingUsageType;
}; };
export type BillingProductPriceDto = {
__typename?: 'BillingProductPriceDTO';
created: Scalars['Float'];
recurringInterval: SubscriptionInterval;
stripePriceId: Scalars['String'];
unitAmount: Scalars['Float'];
};
export type BillingProductPricesOutput = {
__typename?: 'BillingProductPricesOutput';
productPrices: Array<BillingProductPriceDto>;
totalNumberOfPrices: Scalars['Int'];
};
export type BillingSessionOutput = { export type BillingSessionOutput = {
__typename?: 'BillingSessionOutput'; __typename?: 'BillingSessionOutput';
url?: Maybe<Scalars['String']>; url?: Maybe<Scalars['String']>;
@ -1306,7 +1294,6 @@ export type Query = {
getEnvironmentVariablesGrouped: EnvironmentVariablesOutput; getEnvironmentVariablesGrouped: EnvironmentVariablesOutput;
getIndicatorHealthStatus: AdminPanelHealthServiceData; getIndicatorHealthStatus: AdminPanelHealthServiceData;
getPostgresCredentials?: Maybe<PostgresCredentials>; getPostgresCredentials?: Maybe<PostgresCredentials>;
getProductPrices: BillingProductPricesOutput;
getPublicWorkspaceDataByDomain: PublicWorkspaceDataOutput; getPublicWorkspaceDataByDomain: PublicWorkspaceDataOutput;
getQueueMetrics: QueueMetricsData; getQueueMetrics: QueueMetricsData;
getRoles: Array<Role>; getRoles: Array<Role>;
@ -2356,6 +2343,11 @@ export type ValidatePasswordResetTokenQueryVariables = Exact<{
export type ValidatePasswordResetTokenQuery = { __typename?: 'Query', validatePasswordResetToken: { __typename?: 'ValidatePasswordResetToken', id: string, email: string } }; export type ValidatePasswordResetTokenQuery = { __typename?: 'Query', validatePasswordResetToken: { __typename?: 'ValidatePasswordResetToken', id: string, email: string } };
export type BillingBaseProductPricesQueryVariables = Exact<{ [key: string]: never; }>;
export type BillingBaseProductPricesQuery = { __typename?: 'Query', plans: Array<{ __typename?: 'BillingPlanOutput', planKey: BillingPlanKey, baseProduct: { __typename?: 'BillingProductDTO', prices: Array<{ __typename?: 'BillingPriceLicensedDTO', unitAmount: number, stripePriceId: string, recurringInterval: SubscriptionInterval } | { __typename?: 'BillingPriceMeteredDTO' }> } }> };
export type BillingPortalSessionQueryVariables = Exact<{ export type BillingPortalSessionQueryVariables = Exact<{
returnUrlPath?: InputMaybe<Scalars['String']>; returnUrlPath?: InputMaybe<Scalars['String']>;
}>; }>;
@ -2373,13 +2365,6 @@ export type CheckoutSessionMutationVariables = Exact<{
export type CheckoutSessionMutation = { __typename?: 'Mutation', checkoutSession: { __typename?: 'BillingSessionOutput', url?: string | null } }; export type CheckoutSessionMutation = { __typename?: 'Mutation', checkoutSession: { __typename?: 'BillingSessionOutput', url?: string | null } };
export type GetProductPricesQueryVariables = Exact<{
product: Scalars['String'];
}>;
export type GetProductPricesQuery = { __typename?: 'Query', getProductPrices: { __typename?: 'BillingProductPricesOutput', productPrices: Array<{ __typename?: 'BillingProductPriceDTO', created: number, recurringInterval: SubscriptionInterval, stripePriceId: string, unitAmount: number }> } };
export type UpdateBillingSubscriptionMutationVariables = Exact<{ [key: string]: never; }>; export type UpdateBillingSubscriptionMutationVariables = Exact<{ [key: string]: never; }>;
@ -3817,6 +3802,49 @@ export function useValidatePasswordResetTokenLazyQuery(baseOptions?: Apollo.Lazy
export type ValidatePasswordResetTokenQueryHookResult = ReturnType<typeof useValidatePasswordResetTokenQuery>; export type ValidatePasswordResetTokenQueryHookResult = ReturnType<typeof useValidatePasswordResetTokenQuery>;
export type ValidatePasswordResetTokenLazyQueryHookResult = ReturnType<typeof useValidatePasswordResetTokenLazyQuery>; export type ValidatePasswordResetTokenLazyQueryHookResult = ReturnType<typeof useValidatePasswordResetTokenLazyQuery>;
export type ValidatePasswordResetTokenQueryResult = Apollo.QueryResult<ValidatePasswordResetTokenQuery, ValidatePasswordResetTokenQueryVariables>; export type ValidatePasswordResetTokenQueryResult = Apollo.QueryResult<ValidatePasswordResetTokenQuery, ValidatePasswordResetTokenQueryVariables>;
export const BillingBaseProductPricesDocument = gql`
query billingBaseProductPrices {
plans {
planKey
baseProduct {
prices {
... on BillingPriceLicensedDTO {
unitAmount
stripePriceId
recurringInterval
}
}
}
}
}
`;
/**
* __useBillingBaseProductPricesQuery__
*
* To run a query within a React component, call `useBillingBaseProductPricesQuery` and pass it any options that fit your needs.
* When your component renders, `useBillingBaseProductPricesQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useBillingBaseProductPricesQuery({
* variables: {
* },
* });
*/
export function useBillingBaseProductPricesQuery(baseOptions?: Apollo.QueryHookOptions<BillingBaseProductPricesQuery, BillingBaseProductPricesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<BillingBaseProductPricesQuery, BillingBaseProductPricesQueryVariables>(BillingBaseProductPricesDocument, options);
}
export function useBillingBaseProductPricesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<BillingBaseProductPricesQuery, BillingBaseProductPricesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<BillingBaseProductPricesQuery, BillingBaseProductPricesQueryVariables>(BillingBaseProductPricesDocument, options);
}
export type BillingBaseProductPricesQueryHookResult = ReturnType<typeof useBillingBaseProductPricesQuery>;
export type BillingBaseProductPricesLazyQueryHookResult = ReturnType<typeof useBillingBaseProductPricesLazyQuery>;
export type BillingBaseProductPricesQueryResult = Apollo.QueryResult<BillingBaseProductPricesQuery, BillingBaseProductPricesQueryVariables>;
export const BillingPortalSessionDocument = gql` export const BillingPortalSessionDocument = gql`
query BillingPortalSession($returnUrlPath: String) { query BillingPortalSession($returnUrlPath: String) {
billingPortalSession(returnUrlPath: $returnUrlPath) { billingPortalSession(returnUrlPath: $returnUrlPath) {
@ -3893,46 +3921,6 @@ export function useCheckoutSessionMutation(baseOptions?: Apollo.MutationHookOpti
export type CheckoutSessionMutationHookResult = ReturnType<typeof useCheckoutSessionMutation>; export type CheckoutSessionMutationHookResult = ReturnType<typeof useCheckoutSessionMutation>;
export type CheckoutSessionMutationResult = Apollo.MutationResult<CheckoutSessionMutation>; export type CheckoutSessionMutationResult = Apollo.MutationResult<CheckoutSessionMutation>;
export type CheckoutSessionMutationOptions = Apollo.BaseMutationOptions<CheckoutSessionMutation, CheckoutSessionMutationVariables>; export type CheckoutSessionMutationOptions = Apollo.BaseMutationOptions<CheckoutSessionMutation, CheckoutSessionMutationVariables>;
export const GetProductPricesDocument = gql`
query GetProductPrices($product: String!) {
getProductPrices(product: $product) {
productPrices {
created
recurringInterval
stripePriceId
unitAmount
}
}
}
`;
/**
* __useGetProductPricesQuery__
*
* To run a query within a React component, call `useGetProductPricesQuery` and pass it any options that fit your needs.
* When your component renders, `useGetProductPricesQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetProductPricesQuery({
* variables: {
* product: // value for 'product'
* },
* });
*/
export function useGetProductPricesQuery(baseOptions: Apollo.QueryHookOptions<GetProductPricesQuery, GetProductPricesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetProductPricesQuery, GetProductPricesQueryVariables>(GetProductPricesDocument, options);
}
export function useGetProductPricesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetProductPricesQuery, GetProductPricesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetProductPricesQuery, GetProductPricesQueryVariables>(GetProductPricesDocument, options);
}
export type GetProductPricesQueryHookResult = ReturnType<typeof useGetProductPricesQuery>;
export type GetProductPricesLazyQueryHookResult = ReturnType<typeof useGetProductPricesLazyQuery>;
export type GetProductPricesQueryResult = Apollo.QueryResult<GetProductPricesQuery, GetProductPricesQueryVariables>;
export const UpdateBillingSubscriptionDocument = gql` export const UpdateBillingSubscriptionDocument = gql`
mutation UpdateBillingSubscription { mutation UpdateBillingSubscription {
updateBillingSubscription { updateBillingSubscription {

View File

@ -0,0 +1,18 @@
import { gql } from '@apollo/client';
export const BILLING_BASE_PRODUCT_PRICES = gql`
query billingBaseProductPrices {
plans {
planKey
baseProduct {
prices {
... on BillingPriceLicensedDTO {
unitAmount
stripePriceId
recurringInterval
}
}
}
}
}
`;

View File

@ -1,14 +0,0 @@
import { gql } from '@apollo/client';
export const GET_PRODUCT_PRICES = gql`
query GetProductPrices($product: String!) {
getProductPrices(product: $product) {
productPrices {
created
recurringInterval
stripePriceId
unitAmount
}
}
}
`;

View File

@ -0,0 +1,7 @@
import { BillingPriceLicensedDto } from '~/generated/graphql';
export const isBillingPriceLicensed = <T extends { __typename?: string }>(
price: T,
): price is T & BillingPriceLicensedDto => {
return price?.__typename === 'BillingPriceLicensedDTO';
};

View File

@ -6,6 +6,7 @@ import { SubscriptionBenefit } from '@/billing/components/SubscriptionBenefit';
import { SubscriptionPrice } from '@/billing/components/SubscriptionPrice'; import { SubscriptionPrice } from '@/billing/components/SubscriptionPrice';
import { TrialCard } from '@/billing/components/TrialCard'; import { TrialCard } from '@/billing/components/TrialCard';
import { useHandleCheckoutSession } from '@/billing/hooks/useHandleCheckoutSession'; import { useHandleCheckoutSession } from '@/billing/hooks/useHandleCheckoutSession';
import { isBillingPriceLicensed } from '@/billing/utils/isBillingPriceLicensed';
import { billingState } from '@/client-config/states/billingState'; import { billingState } from '@/client-config/states/billingState';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Trans, useLingui } from '@lingui/react/macro'; import { Trans, useLingui } from '@lingui/react/macro';
@ -18,8 +19,12 @@ import {
Loader, Loader,
MainButton, MainButton,
} from 'twenty-ui'; } from 'twenty-ui';
import { SubscriptionInterval } from '~/generated-metadata/graphql'; import {
import { useGetProductPricesQuery } from '~/generated/graphql'; BillingPlanKey,
BillingPriceLicensedDto,
SubscriptionInterval,
useBillingBaseProductPricesQuery,
} from '~/generated/graphql';
const StyledSubscriptionContainer = styled.div<{ const StyledSubscriptionContainer = styled.div<{
withLongerMarginBottom: boolean; withLongerMarginBottom: boolean;
@ -92,13 +97,15 @@ export const ChooseYourPlan = () => {
t`1 000 workflow node executions`, t`1 000 workflow node executions`,
]; ];
const { data: prices } = useGetProductPricesQuery({ const { data: plans } = useBillingBaseProductPricesQuery();
variables: { product: 'base-plan' },
});
const price = prices?.getProductPrices?.productPrices.find( const baseProduct = plans?.plans.find(
(productPrice) => (plan) => plan.planKey === BillingPlanKey.PRO,
productPrice.recurringInterval === SubscriptionInterval.Month, )?.baseProduct;
const baseProductPrice = baseProduct?.prices.find(
(price): price is BillingPriceLicensedDto =>
isBillingPriceLicensed(price) &&
price.recurringInterval === SubscriptionInterval.Month,
); );
const hasWithoutCreditCardTrialPeriod = billing?.trialPeriods.some( const hasWithoutCreditCardTrialPeriod = billing?.trialPeriods.some(
@ -122,12 +129,12 @@ export const ChooseYourPlan = () => {
const handleTrialPeriodChange = (withCreditCard: boolean) => { const handleTrialPeriodChange = (withCreditCard: boolean) => {
return () => { return () => {
if ( if (
isDefined(price) && isDefined(baseProductPrice) &&
billingCheckoutSession.requirePaymentMethod !== withCreditCard billingCheckoutSession.requirePaymentMethod !== withCreditCard
) { ) {
setBillingCheckoutSession({ setBillingCheckoutSession({
plan: billingCheckoutSession.plan, plan: billingCheckoutSession.plan,
interval: price.recurringInterval, interval: baseProductPrice.recurringInterval,
requirePaymentMethod: withCreditCard, requirePaymentMethod: withCreditCard,
}); });
} }
@ -139,7 +146,7 @@ export const ChooseYourPlan = () => {
const withCreditCardTrialPeriodDuration = withCreditCardTrialPeriod?.duration; const withCreditCardTrialPeriodDuration = withCreditCardTrialPeriod?.duration;
return ( return (
isDefined(price) && isDefined(baseProductPrice) &&
isDefined(billing) && ( isDefined(billing) && (
<> <>
<Title noMarginTop> <Title noMarginTop>
@ -163,8 +170,8 @@ export const ChooseYourPlan = () => {
> >
<StyledSubscriptionPriceContainer> <StyledSubscriptionPriceContainer>
<SubscriptionPrice <SubscriptionPrice
type={price.recurringInterval} type={baseProductPrice.recurringInterval}
price={price.unitAmount / 100} price={baseProductPrice.unitAmount / 100}
/> />
</StyledSubscriptionPriceContainer> </StyledSubscriptionPriceContainer>
<StyledBenefitsContainer> <StyledBenefitsContainer>

View File

@ -3,9 +3,14 @@ import { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/testing-library'; import { within } from '@storybook/testing-library';
import { HttpResponse, graphql } from 'msw'; import { HttpResponse, graphql } from 'msw';
import { BILLING_BASE_PRODUCT_PRICES } from '@/billing/graphql/billingBaseProductPrices';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { OnboardingStatus } from '~/generated/graphql'; import {
BillingPlanKey,
OnboardingStatus,
SubscriptionInterval,
} from '~/generated/graphql';
import { ChooseYourPlan } from '~/pages/onboarding/ChooseYourPlan'; import { ChooseYourPlan } from '~/pages/onboarding/ChooseYourPlan';
import { import {
PageDecorator, PageDecorator,
@ -31,31 +36,30 @@ const meta: Meta<PageDecoratorArgs> = {
}, },
}); });
}), }),
graphql.query('GetProductPrices', () => { graphql.query(
return HttpResponse.json({ getOperationName(BILLING_BASE_PRODUCT_PRICES) ?? '',
data: { () => {
getProductPrices: { return HttpResponse.json({
__typename: 'ProductPricesEntity', data: {
productPrices: [ plans: [
{ {
__typename: 'ProductPriceEntity', planKey: BillingPlanKey.PRO,
created: 1699860608, baseProduct: {
recurringInterval: 'Month', prices: [
stripePriceId: 'monthly8usd', {
unitAmount: 900, __typename: 'BillingPriceLicensedDTO',
}, unitAmount: 900,
{ stripePriceId: 'monthly8usd',
__typename: 'ProductPriceEntity', recurringInterval: SubscriptionInterval.Month,
created: 1701874964, },
recurringInterval: 'Year', ],
stripePriceId: 'priceId', },
unitAmount: 9000,
}, },
], ],
}, },
}, });
}); },
}), ),
...graphqlMocks.handlers, ...graphqlMocks.handlers,
], ],
}, },

View File

@ -7,10 +7,8 @@ import { GraphQLError } from 'graphql';
import { isDefined } from 'twenty-shared'; import { isDefined } from 'twenty-shared';
import { BillingCheckoutSessionInput } from 'src/engine/core-modules/billing/dtos/inputs/billing-checkout-session.input'; import { BillingCheckoutSessionInput } from 'src/engine/core-modules/billing/dtos/inputs/billing-checkout-session.input';
import { BillingProductInput } from 'src/engine/core-modules/billing/dtos/inputs/billing-product.input';
import { BillingSessionInput } from 'src/engine/core-modules/billing/dtos/inputs/billing-session.input'; import { BillingSessionInput } from 'src/engine/core-modules/billing/dtos/inputs/billing-session.input';
import { BillingPlanOutput } from 'src/engine/core-modules/billing/dtos/outputs/billing-plan.output'; import { BillingPlanOutput } from 'src/engine/core-modules/billing/dtos/outputs/billing-plan.output';
import { BillingProductPricesOutput } from 'src/engine/core-modules/billing/dtos/outputs/billing-product-prices.output';
import { BillingSessionOutput } from 'src/engine/core-modules/billing/dtos/outputs/billing-session.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 { 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 { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum';
@ -48,28 +46,13 @@ export class BillingResolver {
constructor( constructor(
private readonly billingSubscriptionService: BillingSubscriptionService, private readonly billingSubscriptionService: BillingSubscriptionService,
private readonly billingPortalWorkspaceService: BillingPortalWorkspaceService, private readonly billingPortalWorkspaceService: BillingPortalWorkspaceService,
private readonly stripePriceService: StripePriceService,
private readonly billingPlanService: BillingPlanService, private readonly billingPlanService: BillingPlanService,
private readonly stripePriceService: StripePriceService,
private readonly featureFlagService: FeatureFlagService, private readonly featureFlagService: FeatureFlagService,
private readonly billingService: BillingService, private readonly billingService: BillingService,
private readonly permissionsService: PermissionsService, private readonly permissionsService: PermissionsService,
) {} ) {}
@Query(() => BillingProductPricesOutput)
@UseGuards(WorkspaceAuthGuard)
async getProductPrices(
@AuthWorkspace() workspace: Workspace,
@Args() { product }: BillingProductInput,
) {
const productPrices =
await this.stripePriceService.getStripePrices(product);
return {
totalNumberOfPrices: productPrices.length,
productPrices,
};
}
@Query(() => BillingSessionOutput) @Query(() => BillingSessionOutput)
@UseGuards( @UseGuards(
WorkspaceAuthGuard, WorkspaceAuthGuard,

View File

@ -3,6 +3,7 @@
import { Field, ObjectType } from '@nestjs/graphql'; import { Field, ObjectType } from '@nestjs/graphql';
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum'; import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
@ObjectType() @ObjectType()
export class BillingPriceLicensedDTO { export class BillingPriceLicensedDTO {
@ -14,4 +15,7 @@ export class BillingPriceLicensedDTO {
@Field(() => String) @Field(() => String)
stripePriceId: string; stripePriceId: string;
@Field(() => BillingUsageType)
priceUsageType: BillingUsageType.LICENSED;
} }

View File

@ -5,6 +5,7 @@ import { Field, ObjectType } from '@nestjs/graphql';
import { BillingPriceTierDTO } from 'src/engine/core-modules/billing/dtos/billing-price-tier.dto'; import { BillingPriceTierDTO } from 'src/engine/core-modules/billing/dtos/billing-price-tier.dto';
import { BillingPriceTiersMode } from 'src/engine/core-modules/billing/enums/billing-price-tiers-mode.enum'; import { BillingPriceTiersMode } from 'src/engine/core-modules/billing/enums/billing-price-tiers-mode.enum';
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum'; import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
@ObjectType() @ObjectType()
export class BillingPriceMeteredDTO { export class BillingPriceMeteredDTO {
@ -19,4 +20,7 @@ export class BillingPriceMeteredDTO {
@Field(() => String) @Field(() => String)
stripePriceId: string; stripePriceId: string;
@Field(() => BillingUsageType)
priceUsageType: BillingUsageType.METERED;
} }

View File

@ -4,12 +4,12 @@ import { createUnionType } from '@nestjs/graphql';
import { BillingPriceLicensedDTO } from 'src/engine/core-modules/billing/dtos/billing-price-licensed.dto'; 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 { BillingPriceMeteredDTO } from 'src/engine/core-modules/billing/dtos/billing-price-metered.dto';
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
export const BillingPriceUnionDTO = createUnionType({ export const BillingPriceUnionDTO = createUnionType({
name: 'BillingPriceUnionDTO', name: 'BillingPriceUnionDTO',
types: () => [BillingPriceLicensedDTO, BillingPriceMeteredDTO], types: () => [BillingPriceLicensedDTO, BillingPriceMeteredDTO],
resolveType(value) { resolveType(value) {
if ('unitAmount' in value) { if (value.priceUsageType === BillingUsageType.LICENSED) {
return BillingPriceLicensedDTO; return BillingPriceLicensedDTO;
} }

View File

@ -21,6 +21,6 @@ export class BillingProductDTO {
@Field(() => BillingUsageType) @Field(() => BillingUsageType)
type: BillingUsageType; type: BillingUsageType;
@Field(() => [BillingPriceUnionDTO], { nullable: 'items' }) @Field(() => [BillingPriceUnionDTO])
prices: Array<BillingPriceLicensedDTO | BillingPriceMeteredDTO>; prices: Array<BillingPriceLicensedDTO> | Array<BillingPriceMeteredDTO>;
} }

View File

@ -1,14 +0,0 @@
/* @license Enterprise */
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { BillingProductPriceDTO } from 'src/engine/core-modules/billing/dtos/billing-product-price.dto';
@ObjectType()
export class BillingProductPricesOutput {
@Field(() => Int)
totalNumberOfPrices: number;
@Field(() => [BillingProductPriceDTO])
productPrices: BillingProductPriceDTO[];
}

View File

@ -85,7 +85,7 @@ export class BillingPlanService {
if (!baseProduct) { if (!baseProduct) {
throw new BillingException( throw new BillingException(
'Base product not found', 'Base product not found, did you run the billing:sync-products command?',
BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND, BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND,
); );
} }

View File

@ -26,6 +26,7 @@ import { BillingProductService } from 'src/engine/core-modules/billing/services/
import { StripePriceService } from 'src/engine/core-modules/billing/stripe/services/stripe-price.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 { 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 { 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 { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; 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 { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
@ -176,8 +177,9 @@ export class BillingSubscriptionService {
); );
if (isBillingPlansEnabled) { if (isBillingPlansEnabled) {
const planKey = getPlanKeyFromSubscription(billingSubscription);
const billingProductsByPlan = const billingProductsByPlan =
await this.billingProductService.getProductsByPlan(BillingPlanKey.PRO); await this.billingProductService.getProductsByPlan(planKey);
const pricesPerPlanArray = const pricesPerPlanArray =
this.billingProductService.getProductPricesByInterval({ this.billingProductService.getProductPricesByInterval({
interval: newInterval, interval: newInterval,

View File

@ -19,6 +19,7 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => {
interval: SubscriptionInterval.Month, interval: SubscriptionInterval.Month,
unitAmount: 1000, unitAmount: 1000,
stripePriceId: 'price_base1', stripePriceId: 'price_base1',
priceUsageType: BillingUsageType.LICENSED,
}, },
], ],
}, },
@ -31,6 +32,7 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => {
interval: SubscriptionInterval.Year, interval: SubscriptionInterval.Year,
unitAmount: 2000, unitAmount: 2000,
stripePriceId: 'price_licensed1', stripePriceId: 'price_licensed1',
priceUsageType: BillingUsageType.LICENSED,
}, },
], ],
}, },
@ -51,6 +53,7 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => {
}, },
], ],
stripePriceId: 'price_metered1', stripePriceId: 'price_metered1',
priceUsageType: BillingUsageType.METERED,
}, },
], ],
}, },
@ -71,6 +74,7 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => {
interval: SubscriptionInterval.Month, interval: SubscriptionInterval.Month,
unitAmount: 1000, unitAmount: 1000,
stripePriceId: 'price_base1', stripePriceId: 'price_base1',
priceUsageType: BillingUsageType.LICENSED,
}, },
], ],
type: BillingUsageType.LICENSED, type: BillingUsageType.LICENSED,
@ -79,6 +83,7 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => {
recurringInterval: SubscriptionInterval.Month, recurringInterval: SubscriptionInterval.Month,
unitAmount: 1000, unitAmount: 1000,
stripePriceId: 'price_base1', stripePriceId: 'price_base1',
priceUsageType: BillingUsageType.LICENSED,
}, },
], ],
}, },
@ -91,6 +96,7 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => {
interval: SubscriptionInterval.Year, interval: SubscriptionInterval.Year,
unitAmount: 2000, unitAmount: 2000,
stripePriceId: 'price_licensed1', stripePriceId: 'price_licensed1',
priceUsageType: BillingUsageType.LICENSED,
}, },
], ],
type: BillingUsageType.LICENSED, type: BillingUsageType.LICENSED,
@ -99,6 +105,7 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => {
recurringInterval: SubscriptionInterval.Year, recurringInterval: SubscriptionInterval.Year,
unitAmount: 2000, unitAmount: 2000,
stripePriceId: 'price_licensed1', stripePriceId: 'price_licensed1',
priceUsageType: BillingUsageType.LICENSED,
}, },
], ],
}, },
@ -119,6 +126,7 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => {
}, },
], ],
stripePriceId: 'price_metered1', stripePriceId: 'price_metered1',
priceUsageType: BillingUsageType.METERED,
}, },
], ],
type: BillingUsageType.METERED, type: BillingUsageType.METERED,
@ -134,6 +142,7 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => {
], ],
recurringInterval: SubscriptionInterval.Month, recurringInterval: SubscriptionInterval.Month,
stripePriceId: 'price_metered1', stripePriceId: 'price_metered1',
priceUsageType: BillingUsageType.METERED,
}, },
], ],
}, },
@ -152,6 +161,7 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => {
interval: null, interval: null,
unitAmount: null, unitAmount: null,
stripePriceId: null, stripePriceId: null,
priceUsageType: BillingUsageType.LICENSED,
}, },
], ],
}, },
@ -166,6 +176,7 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => {
tiersMode: null, tiersMode: null,
tiers: null, tiers: null,
stripePriceId: null, stripePriceId: null,
priceUsageType: BillingUsageType.METERED,
}, },
], ],
}, },
@ -186,6 +197,7 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => {
interval: null, interval: null,
unitAmount: null, unitAmount: null,
stripePriceId: null, stripePriceId: null,
priceUsageType: BillingUsageType.LICENSED,
}, },
], ],
type: BillingUsageType.LICENSED, type: BillingUsageType.LICENSED,
@ -194,6 +206,7 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => {
recurringInterval: SubscriptionInterval.Month, recurringInterval: SubscriptionInterval.Month,
unitAmount: 0, unitAmount: 0,
stripePriceId: null, stripePriceId: null,
priceUsageType: BillingUsageType.LICENSED,
}, },
], ],
}, },
@ -208,6 +221,7 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => {
tiersMode: null, tiersMode: null,
tiers: null, tiers: null,
stripePriceId: null, stripePriceId: null,
priceUsageType: BillingUsageType.METERED,
}, },
], ],
type: BillingUsageType.METERED, type: BillingUsageType.METERED,
@ -217,6 +231,7 @@ describe('formatBillingDatabaseProductToGraphqlDTO', () => {
tiers: [], tiers: [],
recurringInterval: SubscriptionInterval.Month, recurringInterval: SubscriptionInterval.Month,
stripePriceId: null, stripePriceId: null,
priceUsageType: BillingUsageType.METERED,
}, },
], ],
}, },

View File

@ -58,6 +58,7 @@ const formatBillingDatabasePriceToMeteredPriceDTO = (
})) ?? [], })) ?? [],
recurringInterval: billingPrice?.interval ?? SubscriptionInterval.Month, recurringInterval: billingPrice?.interval ?? SubscriptionInterval.Month,
stripePriceId: billingPrice?.stripePriceId, stripePriceId: billingPrice?.stripePriceId,
priceUsageType: BillingUsageType.METERED,
}; };
}; };
@ -68,5 +69,6 @@ const formatBillingDatabasePriceToLicensedPriceDTO = (
recurringInterval: billingPrice?.interval ?? SubscriptionInterval.Month, recurringInterval: billingPrice?.interval ?? SubscriptionInterval.Month,
unitAmount: billingPrice?.unitAmount ?? 0, unitAmount: billingPrice?.unitAmount ?? 0,
stripePriceId: billingPrice?.stripePriceId, stripePriceId: billingPrice?.stripePriceId,
priceUsageType: BillingUsageType.LICENSED,
}; };
}; };

View File

@ -0,0 +1,17 @@
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
export const getPlanKeyFromSubscription = (
subscription: BillingSubscription,
): BillingPlanKey => {
const planKey = subscription.metadata?.planKey;
switch (planKey) {
case 'PRO':
return BillingPlanKey.PRO;
case 'ENTERPRISE':
return BillingPlanKey.ENTERPRISE;
default:
return BillingPlanKey.PRO;
}
};