add fetch billing products from tables instead of env variables (#9601)

Solves https://github.com/twentyhq/private-issues/issues/237

**TLDR:**

- Fetches billing products and prices from the tables BilllingProducts
and BillingPrices instead of fetching the product from the environment
variables and the prices from the stripe API.
- Adds new feature flag for this feature
- Fixes calls used to fetch stripe products and prices for the command
Billing Sync Plans Data.


**In order to test:**

1. Have the environment variable IS_BILLING_ENABLED set to true and add
the other required environment variables for Billing to work
2. Do a database reset (to ensure that the new feature flag is properly
added and that the billing tables are created)
3. 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)
4. Run the server , the frontend, the worker, and the stripe listen
command (`stripe listen --forward-to
http://localhost:3000/billing/webhooks`)
5. Buy a subscription for the Acme workspace and play with the project

**Doing**

I think there is some room of progress for the function
formatProductPrices, I used a similar version that was done before, I'll
look into that.
This commit is contained in:
Ana Sofia Marin Alexandre
2025-01-21 16:19:29 -03:00
committed by GitHub
parent 3d2bb03c6d
commit 7d30b7577d
59 changed files with 870 additions and 245 deletions

View File

@ -50,6 +50,11 @@ export const seedFeatureFlags = async (
workspaceId: workspaceId, workspaceId: workspaceId,
value: true, value: true,
}, },
{
key: FeatureFlagKey.IsBillingPlansEnabled,
workspaceId: workspaceId,
value: true,
},
{ {
key: FeatureFlagKey.IsGmailSendEmailScopeEnabled, key: FeatureFlagKey.IsGmailSendEmailScopeEnabled,
workspaceId: workspaceId, workspaceId: workspaceId,

View File

@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddNonNullableProductDescription1737127856478
implements MigrationInterface
{
name = 'AddNonNullableProductDescription1737127856478';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."billingProduct" ALTER COLUMN "description" SET DEFAULT ''`,
);
await queryRunner.query(
`ALTER TABLE "core"."billingProduct" ALTER COLUMN "description" SET NOT NULL`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."billingProduct" ALTER COLUMN "description" DROP DEFAULT`,
);
await queryRunner.query(
`ALTER TABLE "core"."billingProduct" ALTER COLUMN "description" DROP NOT NULL`,
);
}
}

View File

@ -19,11 +19,11 @@ import {
import { BillingWebhookEvent } from 'src/engine/core-modules/billing/enums/billing-webhook-events.enum'; import { BillingWebhookEvent } from 'src/engine/core-modules/billing/enums/billing-webhook-events.enum';
import { BillingRestApiExceptionFilter } from 'src/engine/core-modules/billing/filters/billing-api-exception.filter'; import { BillingRestApiExceptionFilter } from 'src/engine/core-modules/billing/filters/billing-api-exception.filter';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing/services/billing-webhook-entitlement.service';
import { BillingWebhookPriceService } from 'src/engine/core-modules/billing/services/billing-webhook-price.service';
import { BillingWebhookProductService } from 'src/engine/core-modules/billing/services/billing-webhook-product.service';
import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing/services/billing-webhook-subscription.service';
import { StripeWebhookService } from 'src/engine/core-modules/billing/stripe/services/stripe-webhook.service'; import { StripeWebhookService } from 'src/engine/core-modules/billing/stripe/services/stripe-webhook.service';
import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-entitlement.service';
import { BillingWebhookPriceService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-price.service';
import { BillingWebhookProductService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-product.service';
import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-subscription.service';
@Controller('billing') @Controller('billing')
@UseFilters(BillingRestApiExceptionFilter) @UseFilters(BillingRestApiExceptionFilter)
export class BillingController { export class BillingController {

View File

@ -12,5 +12,6 @@ export class BillingException extends CustomException {
export enum BillingExceptionCode { export enum BillingExceptionCode {
BILLING_CUSTOMER_NOT_FOUND = 'BILLING_CUSTOMER_NOT_FOUND', BILLING_CUSTOMER_NOT_FOUND = 'BILLING_CUSTOMER_NOT_FOUND',
BILLING_PRODUCT_NOT_FOUND = 'BILLING_PRODUCT_NOT_FOUND', BILLING_PRODUCT_NOT_FOUND = 'BILLING_PRODUCT_NOT_FOUND',
BILLING_PRICE_NOT_FOUND = 'BILLING_PRICE_NOT_FOUND',
BILLING_SUBSCRIPTION_EVENT_WORKSPACE_NOT_FOUND = 'BILLING_SUBSCRIPTION_EVENT_WORKSPACE_NOT_FOUND', BILLING_SUBSCRIPTION_EVENT_WORKSPACE_NOT_FOUND = 'BILLING_SUBSCRIPTION_EVENT_WORKSPACE_NOT_FOUND',
} }

View File

@ -14,14 +14,15 @@ import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entitie
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { BillingRestApiExceptionFilter } from 'src/engine/core-modules/billing/filters/billing-api-exception.filter'; import { BillingRestApiExceptionFilter } from 'src/engine/core-modules/billing/filters/billing-api-exception.filter';
import { BillingWorkspaceMemberListener } from 'src/engine/core-modules/billing/listeners/billing-workspace-member.listener'; import { BillingWorkspaceMemberListener } from 'src/engine/core-modules/billing/listeners/billing-workspace-member.listener';
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 { 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 { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing/services/billing-webhook-entitlement.service';
import { BillingWebhookPriceService } from 'src/engine/core-modules/billing/services/billing-webhook-price.service';
import { BillingWebhookProductService } from 'src/engine/core-modules/billing/services/billing-webhook-product.service';
import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing/services/billing-webhook-subscription.service';
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service'; import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module'; import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module';
import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-entitlement.service';
import { BillingWebhookPriceService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-price.service';
import { BillingWebhookProductService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-product.service';
import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-subscription.service';
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
@ -56,6 +57,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
BillingWebhookEntitlementService, BillingWebhookEntitlementService,
BillingPortalWorkspaceService, BillingPortalWorkspaceService,
BillingResolver, BillingResolver,
BillingPlanService,
BillingWorkspaceMemberListener, BillingWorkspaceMemberListener,
BillingService, BillingService,
BillingWebhookProductService, BillingWebhookProductService,

View File

@ -1,16 +1,24 @@
import { UseGuards } from '@nestjs/common'; import { UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { BillingSessionInput } from 'src/engine/core-modules/billing/dto/billing-session.input'; import { GraphQLError } from 'graphql';
import { CheckoutSessionInput } from 'src/engine/core-modules/billing/dto/checkout-session.input';
import { ProductPricesEntity } from 'src/engine/core-modules/billing/dto/product-prices.entity'; import { BillingCheckoutSessionInput } from 'src/engine/core-modules/billing/dtos/inputs/billing-checkout-session.input';
import { ProductInput } from 'src/engine/core-modules/billing/dto/product.input'; import { BillingProductInput } from 'src/engine/core-modules/billing/dtos/inputs/billing-product.input';
import { SessionEntity } from 'src/engine/core-modules/billing/dto/session.entity'; import { BillingSessionInput } from 'src/engine/core-modules/billing/dtos/inputs/billing-session.input';
import { UpdateBillingEntity } from 'src/engine/core-modules/billing/dto/update-billing.entity'; 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 { 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';
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 { 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 { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
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 { 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';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { User } from 'src/engine/core-modules/user/user.entity'; import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
@ -24,20 +32,26 @@ export class BillingResolver {
private readonly billingSubscriptionService: BillingSubscriptionService, private readonly billingSubscriptionService: BillingSubscriptionService,
private readonly billingPortalWorkspaceService: BillingPortalWorkspaceService, private readonly billingPortalWorkspaceService: BillingPortalWorkspaceService,
private readonly stripePriceService: StripePriceService, private readonly stripePriceService: StripePriceService,
private readonly billingPlanService: BillingPlanService,
private readonly featureFlagService: FeatureFlagService,
) {} ) {}
@Query(() => ProductPricesEntity) @Query(() => BillingProductPricesOutput)
async getProductPrices(@Args() { product }: ProductInput) { @UseGuards(WorkspaceAuthGuard)
async getProductPrices(
@AuthWorkspace() workspace: Workspace,
@Args() { product }: BillingProductInput,
) {
const productPrices = const productPrices =
await this.stripePriceService.getStripePrices(product); await this.stripePriceService.getStripePrices(product);
return { return {
totalNumberOfPrices: productPrices.length, totalNumberOfPrices: productPrices.length,
productPrices: productPrices, productPrices,
}; };
} }
@Query(() => SessionEntity) @Query(() => BillingSessionOutput)
@UseGuards(WorkspaceAuthGuard) @UseGuards(WorkspaceAuthGuard)
async billingPortalSession( async billingPortalSession(
@AuthWorkspace() workspace: Workspace, @AuthWorkspace() workspace: Workspace,
@ -51,7 +65,7 @@ export class BillingResolver {
}; };
} }
@Mutation(() => SessionEntity) @Mutation(() => BillingSessionOutput)
@UseGuards(WorkspaceAuthGuard, UserAuthGuard) @UseGuards(WorkspaceAuthGuard, UserAuthGuard)
async checkoutSession( async checkoutSession(
@AuthWorkspace() workspace: Workspace, @AuthWorkspace() workspace: Workspace,
@ -62,15 +76,37 @@ export class BillingResolver {
successUrlPath, successUrlPath,
plan, plan,
requirePaymentMethod, requirePaymentMethod,
}: CheckoutSessionInput, }: BillingCheckoutSessionInput,
) { ) {
const productPrice = await this.stripePriceService.getStripePrice( const isBillingPlansEnabled =
AvailableProduct.BasePlan, await this.featureFlagService.isFeatureEnabled(
recurringInterval, FeatureFlagKey.IsBillingPlansEnabled,
); workspace.id,
);
let productPrice;
if (isBillingPlansEnabled) {
const baseProduct = await this.billingPlanService.getPlanBaseProduct(
plan ?? BillingPlanKey.PRO,
);
if (!baseProduct) {
throw new GraphQLError('Base product not found');
}
productPrice = baseProduct.billingPrices.find(
(price) => price.interval === recurringInterval,
);
} else {
productPrice = await this.stripePriceService.getStripePrice(
AvailableProduct.BasePlan,
recurringInterval,
);
}
if (!productPrice) { if (!productPrice) {
throw new Error( throw new GraphQLError(
'Product price not found for the given recurring interval', 'Product price not found for the given recurring interval',
); );
} }
@ -87,11 +123,19 @@ export class BillingResolver {
}; };
} }
@Mutation(() => UpdateBillingEntity) @Mutation(() => BillingUpdateOutput)
@UseGuards(WorkspaceAuthGuard) @UseGuards(WorkspaceAuthGuard)
async updateBillingSubscription(@AuthWorkspace() workspace: Workspace) { async updateBillingSubscription(@AuthWorkspace() workspace: Workspace) {
await this.billingSubscriptionService.applyBillingSubscription(workspace); await this.billingSubscriptionService.applyBillingSubscription(workspace);
return { success: true }; return { success: true };
} }
@Query(() => [BillingPlanOutput])
@UseGuards(WorkspaceAuthGuard)
async plans(): Promise<BillingPlanOutput[]> {
const plans = await this.billingPlanService.getPlans();
return plans.map(formatBillingDatabaseProductToGraphqlDTO);
}
} }

View File

@ -15,9 +15,9 @@ import { StripeBillingMeterService } from 'src/engine/core-modules/billing/strip
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 { StripeProductService } from 'src/engine/core-modules/billing/stripe/services/stripe-product.service'; import { StripeProductService } from 'src/engine/core-modules/billing/stripe/services/stripe-product.service';
import { isStripeValidProductMetadata } from 'src/engine/core-modules/billing/utils/is-stripe-valid-product-metadata.util'; import { isStripeValidProductMetadata } from 'src/engine/core-modules/billing/utils/is-stripe-valid-product-metadata.util';
import { transformStripeMeterDataToMeterRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-meter-data-to-meter-repository-data.util'; import { transformStripeMeterToDatabaseMeter } from 'src/engine/core-modules/billing/utils/transform-stripe-meter-to-database-meter.util';
import { transformStripePriceDataToPriceRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-price-data-to-price-repository-data.util'; import { transformStripePriceToDatabasePrice } from 'src/engine/core-modules/billing/utils/transform-stripe-price-to-database-price.util';
import { transformStripeProductDataToProductRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-product-data-to-product-repository-data.util'; import { transformStripeProductToDatabaseProduct } from 'src/engine/core-modules/billing/utils/transform-stripe-product-to-database-product.util';
@Command({ @Command({
name: 'billing:sync-plans-data', name: 'billing:sync-plans-data',
description: description:
@ -47,7 +47,7 @@ export class BillingSyncPlansDataCommand extends BaseCommandRunner {
try { try {
if (!options.dryRun) { if (!options.dryRun) {
await this.billingMeterRepository.upsert( await this.billingMeterRepository.upsert(
transformStripeMeterDataToMeterRepositoryData(meter), transformStripeMeterToDatabaseMeter(meter),
{ {
conflictPaths: ['stripeMeterId'], conflictPaths: ['stripeMeterId'],
}, },
@ -67,7 +67,7 @@ export class BillingSyncPlansDataCommand extends BaseCommandRunner {
try { try {
if (!options.dryRun) { if (!options.dryRun) {
await this.billingProductRepository.upsert( await this.billingProductRepository.upsert(
transformStripeProductDataToProductRepositoryData(product), transformStripeProductToDatabaseProduct(product),
{ {
conflictPaths: ['stripeProductId'], conflictPaths: ['stripeProductId'],
}, },
@ -148,9 +148,7 @@ export class BillingSyncPlansDataCommand extends BaseCommandRunner {
options, options,
); );
const transformedPrices = billingPrices.flatMap((prices) => const transformedPrices = billingPrices.flatMap((prices) =>
prices.map((price) => prices.map((price) => transformStripePriceToDatabasePrice(price)),
transformStripePriceDataToPriceRepositoryData(price),
),
); );
this.logger.log(`Upserting ${transformedPrices.length} transformed prices`); this.logger.log(`Upserting ${transformedPrices.length} transformed prices`);

View File

@ -1,12 +0,0 @@
import { Field, Int, ObjectType } from '@nestjs/graphql';
import { ProductPriceEntity } from 'src/engine/core-modules/billing/dto/product-price.entity';
@ObjectType()
export class ProductPricesEntity {
@Field(() => Int)
totalNumberOfPrices: number;
@Field(() => [ProductPriceEntity])
productPrices: ProductPriceEntity[];
}

View File

@ -0,0 +1,15 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
@ObjectType()
export class BillingPriceLicensedDTO {
@Field(() => SubscriptionInterval)
recurringInterval: SubscriptionInterval;
@Field(() => Number)
unitAmount: number;
@Field(() => String)
stripePriceId: string;
}

View File

@ -0,0 +1,20 @@
import { Field, ObjectType } from '@nestjs/graphql';
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 { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
@ObjectType()
export class BillingPriceMeteredDTO {
@Field(() => BillingPriceTiersMode, { nullable: true })
tiersMode: BillingPriceTiersMode.GRADUATED | null;
@Field(() => [BillingPriceTierDTO], { nullable: true })
tiers: BillingPriceTierDTO[];
@Field(() => SubscriptionInterval)
recurringInterval: SubscriptionInterval;
@Field(() => String)
stripePriceId: string;
}

View File

@ -0,0 +1,13 @@
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class BillingPriceTierDTO {
@Field(() => Number, { nullable: true })
upTo: number | null;
@Field(() => Number, { nullable: true })
flatAmount: number | null;
@Field(() => Number, { nullable: true })
unitAmount: number | null;
}

View File

@ -0,0 +1,16 @@
import { createUnionType } 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';
export const BillingPriceUnionDTO = createUnionType({
name: 'BillingPriceUnionDTO',
types: () => [BillingPriceLicensedDTO, BillingPriceMeteredDTO],
resolveType(value) {
if ('unitAmount' in value) {
return BillingPriceLicensedDTO;
}
return BillingPriceMeteredDTO;
},
});

View File

@ -4,7 +4,7 @@ import Stripe from 'stripe';
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';
@ObjectType() @ObjectType()
export class ProductPriceEntity { export class BillingProductPriceDTO {
@Field(() => SubscriptionInterval) @Field(() => SubscriptionInterval)
recurringInterval: Stripe.Price.Recurring.Interval; recurringInterval: Stripe.Price.Recurring.Interval;

View File

@ -0,0 +1,24 @@
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';
@ObjectType()
export class BillingProductDTO {
@Field(() => String)
name: string;
@Field(() => String)
description: string;
@Field(() => [String], { nullable: true })
images: string[];
@Field(() => BillingUsageType)
type: BillingUsageType;
@Field(() => [BillingPriceUnionDTO], { nullable: 'items' })
prices: Array<BillingPriceLicensedDTO | BillingPriceMeteredDTO>;
}

View File

@ -3,7 +3,7 @@ import { Field, ObjectType } from '@nestjs/graphql';
import { Min } from 'class-validator'; import { Min } from 'class-validator';
@ObjectType() @ObjectType()
export class TrialPeriodDTO { export class BillingTrialPeriodDTO {
@Field(() => Number) @Field(() => Number)
@Min(0) @Min(0)
duration: number; duration: number;

View File

@ -12,7 +12,7 @@ import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-pl
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';
@ArgsType() @ArgsType()
export class CheckoutSessionInput { export class BillingCheckoutSessionInput {
@Field(() => SubscriptionInterval) @Field(() => SubscriptionInterval)
@IsEnum(SubscriptionInterval) @IsEnum(SubscriptionInterval)
@IsNotEmpty() @IsNotEmpty()

View File

@ -5,7 +5,7 @@ import { IsNotEmpty, IsString } from 'class-validator';
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';
@ArgsType() @ArgsType()
export class ProductInput { export class BillingProductInput {
@Field(() => String) @Field(() => String)
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()

View File

@ -0,0 +1,19 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { BillingProductDTO } from 'src/engine/core-modules/billing/dtos/billing-product.dto';
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
@ObjectType()
export class BillingPlanOutput {
@Field(() => BillingPlanKey)
planKey: BillingPlanKey;
@Field(() => BillingProductDTO)
baseProduct: BillingProductDTO;
@Field(() => [BillingProductDTO])
otherLicensedProducts: BillingProductDTO[];
@Field(() => [BillingProductDTO])
meteredProducts: BillingProductDTO[];
}

View File

@ -0,0 +1,12 @@
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

@ -1,7 +1,7 @@
import { Field, ObjectType } from '@nestjs/graphql'; import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType() @ObjectType()
export class SessionEntity { export class BillingSessionOutput {
@Field(() => String, { nullable: true }) @Field(() => String, { nullable: true })
url: string; url: string;
} }

View File

@ -1,7 +1,7 @@
import { Field, ObjectType } from '@nestjs/graphql'; import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType() @ObjectType()
export class UpdateBillingEntity { export class BillingUpdateOutput {
@Field(() => Boolean, { @Field(() => Boolean, {
description: 'Boolean that confirms query was successful', description: 'Boolean that confirms query was successful',
}) })

View File

@ -32,8 +32,8 @@ export class BillingProduct {
@Column({ nullable: false }) @Column({ nullable: false })
active: boolean; active: boolean;
@Column({ nullable: true, type: 'text' }) @Column({ nullable: false, type: 'text', default: '' })
description: string | null; description: string;
@Column({ nullable: false }) @Column({ nullable: false })
name: string; name: string;

View File

@ -1,4 +1,10 @@
import { registerEnumType } from '@nestjs/graphql';
export enum BillingPriceTiersMode { export enum BillingPriceTiersMode {
GRADUATED = 'GRADUATED', GRADUATED = 'GRADUATED',
VOLUME = 'VOLUME', VOLUME = 'VOLUME',
} }
registerEnumType(BillingPriceTiersMode, {
name: 'BillingPriceTiersMode',
description: 'The different billing price tiers modes',
});

View File

@ -0,0 +1,107 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import {
BillingException,
BillingExceptionCode,
} from 'src/engine/core-modules/billing/billing.exception';
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 { BillingGetPlanResult } from 'src/engine/core-modules/billing/types/billing-get-plan-result.type';
@Injectable()
export class BillingPlanService {
protected readonly logger = new Logger(BillingPlanService.name);
constructor(
@InjectRepository(BillingProduct, 'core')
private readonly billingProductRepository: Repository<BillingProduct>,
) {}
async getProductsByProductMetadata({
planKey,
priceUsageBased,
isBaseProduct,
}: {
planKey: BillingPlanKey;
priceUsageBased: BillingUsageType;
isBaseProduct: 'true' | 'false';
}): Promise<BillingProduct[]> {
const products = await this.billingProductRepository.find({
where: {
metadata: {
planKey,
priceUsageBased,
isBaseProduct,
},
active: true,
},
relations: ['billingPrices'],
});
return products;
}
async getPlanBaseProduct(planKey: BillingPlanKey): Promise<BillingProduct> {
const [baseProduct] = await this.getProductsByProductMetadata({
planKey,
priceUsageBased: BillingUsageType.LICENSED,
isBaseProduct: 'true',
});
return baseProduct;
}
async getPlans(): Promise<BillingGetPlanResult[]> {
const planKeys = Object.values(BillingPlanKey);
const products = await this.billingProductRepository.find({
where: {
active: true,
},
relations: ['billingPrices'],
});
return planKeys.map((planKey) => {
const planProducts = products
.filter((product) => product.metadata.planKey === planKey)
.map((product) => {
return {
...product,
billingPrices: product.billingPrices.filter(
(price) => price.active,
),
};
});
const baseProduct = planProducts.find(
(product) => product.metadata.isBaseProduct === 'true',
);
if (!baseProduct) {
throw new BillingException(
'Base product not found',
BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND,
);
}
const meteredProducts = planProducts.filter(
(product) =>
product.metadata.priceUsageBased === BillingUsageType.METERED,
);
const otherLicensedProducts = planProducts.filter(
(product) =>
product.metadata.priceUsageBased === BillingUsageType.LICENSED &&
product.metadata.isBaseProduct === 'false',
);
return {
planKey,
baseProduct,
meteredProducts,
otherLicensedProducts,
};
});
}
}

View File

@ -6,18 +6,25 @@ import assert from 'assert';
import Stripe from 'stripe'; import Stripe from 'stripe';
import { Not, Repository } from 'typeorm'; import { Not, Repository } from 'typeorm';
import {
BillingException,
BillingExceptionCode,
} from 'src/engine/core-modules/billing/billing.exception';
import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity'; import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.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 { 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 { 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 { 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 { 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 { 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 { 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 { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Injectable() @Injectable()
export class BillingSubscriptionService { export class BillingSubscriptionService {
protected readonly logger = new Logger(BillingSubscriptionService.name); protected readonly logger = new Logger(BillingSubscriptionService.name);
@ -25,7 +32,9 @@ export class BillingSubscriptionService {
private readonly stripeSubscriptionService: StripeSubscriptionService, private readonly stripeSubscriptionService: StripeSubscriptionService,
private readonly stripePriceService: StripePriceService, private readonly stripePriceService: StripePriceService,
private readonly stripeSubscriptionItemService: StripeSubscriptionItemService, private readonly stripeSubscriptionItemService: StripeSubscriptionItemService,
private readonly billingPlanService: BillingPlanService,
private readonly environmentService: EnvironmentService, private readonly environmentService: EnvironmentService,
private readonly featureFlagService: FeatureFlagService,
@InjectRepository(BillingEntitlement, 'core') @InjectRepository(BillingEntitlement, 'core')
private readonly billingEntitlementRepository: Repository<BillingEntitlement>, private readonly billingEntitlementRepository: Repository<BillingEntitlement>,
@InjectRepository(BillingSubscription, 'core') @InjectRepository(BillingSubscription, 'core')
@ -56,19 +65,37 @@ export class BillingSubscriptionService {
'BILLING_STRIPE_BASE_PLAN_PRODUCT_ID', 'BILLING_STRIPE_BASE_PLAN_PRODUCT_ID',
), ),
) { ) {
const isBillingPlansEnabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsBillingPlansEnabled,
workspaceId,
);
const billingSubscription = await this.getCurrentBillingSubscriptionOrThrow( const billingSubscription = await this.getCurrentBillingSubscriptionOrThrow(
{ workspaceId }, { workspaceId },
); );
const getStripeProductId = isBillingPlansEnabled
? (await this.billingPlanService.getPlanBaseProduct(BillingPlanKey.PRO))
?.stripeProductId
: stripeProductId;
if (!getStripeProductId) {
throw new BillingException(
'Base product not found',
BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND,
);
}
const billingSubscriptionItem = const billingSubscriptionItem =
billingSubscription.billingSubscriptionItems.filter( billingSubscription.billingSubscriptionItems.filter(
(billingSubscriptionItem) => (billingSubscriptionItem) =>
billingSubscriptionItem.stripeProductId === stripeProductId, billingSubscriptionItem.stripeProductId === getStripeProductId,
)?.[0]; )?.[0];
if (!billingSubscriptionItem) { if (!billingSubscriptionItem) {
throw new Error( throw new Error(
`Cannot find billingSubscriptionItem for product ${stripeProductId} for workspace ${workspaceId}`, `Cannot find billingSubscriptionItem for product ${getStripeProductId} for workspace ${workspaceId}`,
); );
} }
@ -127,7 +154,11 @@ export class BillingSubscriptionService {
const billingSubscription = await this.getCurrentBillingSubscriptionOrThrow( const billingSubscription = await this.getCurrentBillingSubscriptionOrThrow(
{ workspaceId: workspace.id }, { workspaceId: workspace.id },
); );
const isBillingPlansEnabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsBillingPlansEnabled,
workspace.id,
);
const newInterval = const newInterval =
billingSubscription?.interval === SubscriptionInterval.Year billingSubscription?.interval === SubscriptionInterval.Year
? SubscriptionInterval.Month ? SubscriptionInterval.Month
@ -136,10 +167,29 @@ export class BillingSubscriptionService {
const billingSubscriptionItem = const billingSubscriptionItem =
await this.getCurrentBillingSubscriptionItemOrThrow(workspace.id); await this.getCurrentBillingSubscriptionItemOrThrow(workspace.id);
const productPrice = await this.stripePriceService.getStripePrice( let productPrice;
AvailableProduct.BasePlan,
newInterval, if (isBillingPlansEnabled) {
); const baseProduct = await this.billingPlanService.getPlanBaseProduct(
BillingPlanKey.PRO,
);
if (!baseProduct) {
throw new BillingException(
'Base product not found',
BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND,
);
}
productPrice = baseProduct.billingPrices.find(
(price) => price.interval === newInterval,
);
} else {
productPrice = await this.stripePriceService.getStripePrice(
AvailableProduct.BasePlan,
newInterval,
);
}
if (!productPrice) { if (!productPrice) {
throw new Error( throw new Error(

View File

@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
import Stripe from 'stripe'; import Stripe from 'stripe';
import { ProductPriceEntity } from 'src/engine/core-modules/billing/dto/product-price.entity'; 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 { 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 { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
@ -46,10 +46,10 @@ export class StripePriceService {
if (product === AvailableProduct.BasePlan) { if (product === AvailableProduct.BasePlan) {
return this.environmentService.get('BILLING_STRIPE_BASE_PLAN_PRODUCT_ID'); return this.environmentService.get('BILLING_STRIPE_BASE_PLAN_PRODUCT_ID');
} }
} // PD:,will be eliminated after refactoring }
formatProductPrices(prices: Stripe.Price[]): ProductPriceEntity[] { formatProductPrices(prices: Stripe.Price[]): BillingProductPriceDTO[] {
const productPrices: ProductPriceEntity[] = Object.values( const productPrices: BillingProductPriceDTO[] = Object.values(
prices prices
.filter((item) => item.recurring?.interval && item.unit_amount) .filter((item) => item.recurring?.interval && item.unit_amount)
.reduce((acc, item: Stripe.Price) => { .reduce((acc, item: Stripe.Price) => {
@ -68,7 +68,7 @@ export class StripePriceService {
}; };
} }
return acc satisfies Record<string, ProductPriceEntity>; return acc satisfies Record<string, BillingProductPriceDTO>;
}, {}), }, {}),
); );
@ -76,8 +76,10 @@ export class StripePriceService {
} }
async getPricesByProductId(productId: string) { async getPricesByProductId(productId: string) {
const prices = await this.stripe.prices.search({ const prices = await this.stripe.prices.list({
query: `product:'${productId}'`, product: productId,
type: 'recurring',
expand: ['data.currency_options', 'data.tiers'],
}); });
return prices.data; return prices.data;

View File

@ -23,7 +23,10 @@ export class StripeProductService {
} }
async getAllProducts() { async getAllProducts() {
const products = await this.stripe.products.list(); const products = await this.stripe.products.list({
active: true,
limit: 100,
});
return products.data; return products.data;
} }

View File

@ -0,0 +1,9 @@
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';
export type BillingGetPlanResult = {
planKey: BillingPlanKey;
baseProduct: BillingProduct;
meteredProducts: BillingProduct[];
otherLicensedProducts: BillingProduct[];
};

View File

@ -5,6 +5,7 @@ export type BillingProductMetadata =
| { | {
planKey: BillingPlanKey; planKey: BillingPlanKey;
priceUsageBased: BillingUsageType; priceUsageBased: BillingUsageType;
isBaseProduct: 'true' | 'false';
[key: string]: string; [key: string]: string;
} }
| Record<string, never>; | Record<string, never>;

View File

@ -0,0 +1,224 @@
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.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 { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
import { BillingGetPlanResult } from 'src/engine/core-modules/billing/types/billing-get-plan-result.type';
import { formatBillingDatabaseProductToGraphqlDTO } from 'src/engine/core-modules/billing/utils/format-database-product-to-graphql-dto.util';
describe('formatBillingDatabaseProductToGraphqlDTO', () => {
it('should format a complete billing plan correctly', () => {
const mockPlan = {
planKey: BillingPlanKey.PRO,
baseProduct: {
id: 'base-1',
name: 'Base Product',
billingPrices: [
{
interval: SubscriptionInterval.Month,
unitAmount: 1000,
stripePriceId: 'price_base1',
},
],
},
otherLicensedProducts: [
{
id: 'licensed-1',
name: 'Licensed Product',
billingPrices: [
{
interval: SubscriptionInterval.Year,
unitAmount: 2000,
stripePriceId: 'price_licensed1',
},
],
},
],
meteredProducts: [
{
id: 'metered-1',
name: 'Metered Product',
billingPrices: [
{
interval: SubscriptionInterval.Month,
tiersMode: BillingPriceTiersMode.GRADUATED,
tiers: [
{
up_to: 10,
flat_amount: 1000,
unit_amount: 100,
},
],
stripePriceId: 'price_metered1',
},
],
},
],
};
const result = formatBillingDatabaseProductToGraphqlDTO(
mockPlan as unknown as BillingGetPlanResult,
);
expect(result).toEqual({
planKey: BillingPlanKey.PRO,
baseProduct: {
id: 'base-1',
name: 'Base Product',
billingPrices: [
{
interval: SubscriptionInterval.Month,
unitAmount: 1000,
stripePriceId: 'price_base1',
},
],
type: BillingUsageType.LICENSED,
prices: [
{
recurringInterval: SubscriptionInterval.Month,
unitAmount: 1000,
stripePriceId: 'price_base1',
},
],
},
otherLicensedProducts: [
{
id: 'licensed-1',
name: 'Licensed Product',
billingPrices: [
{
interval: SubscriptionInterval.Year,
unitAmount: 2000,
stripePriceId: 'price_licensed1',
},
],
type: BillingUsageType.LICENSED,
prices: [
{
recurringInterval: SubscriptionInterval.Year,
unitAmount: 2000,
stripePriceId: 'price_licensed1',
},
],
},
],
meteredProducts: [
{
id: 'metered-1',
name: 'Metered Product',
billingPrices: [
{
interval: SubscriptionInterval.Month,
tiersMode: BillingPriceTiersMode.GRADUATED,
tiers: [
{
up_to: 10,
flat_amount: 1000,
unit_amount: 100,
},
],
stripePriceId: 'price_metered1',
},
],
type: BillingUsageType.METERED,
prices: [
{
tiersMode: BillingPriceTiersMode.GRADUATED,
tiers: [
{
upTo: 10,
flatAmount: 1000,
unitAmount: 100,
},
],
recurringInterval: SubscriptionInterval.Month,
stripePriceId: 'price_metered1',
},
],
},
],
});
});
it('should handle empty products and null values', () => {
const mockPlan = {
planKey: 'empty-plan',
baseProduct: {
id: 'base-1',
name: 'Base Product',
billingPrices: [
{
interval: null,
unitAmount: null,
stripePriceId: null,
},
],
},
otherLicensedProducts: [],
meteredProducts: [
{
id: 'metered-1',
name: 'Metered Product',
billingPrices: [
{
interval: null,
tiersMode: null,
tiers: null,
stripePriceId: null,
},
],
},
],
};
const result = formatBillingDatabaseProductToGraphqlDTO(
mockPlan as unknown as BillingGetPlanResult,
);
expect(result).toEqual({
planKey: 'empty-plan',
baseProduct: {
id: 'base-1',
name: 'Base Product',
billingPrices: [
{
interval: null,
unitAmount: null,
stripePriceId: null,
},
],
type: BillingUsageType.LICENSED,
prices: [
{
recurringInterval: SubscriptionInterval.Month,
unitAmount: 0,
stripePriceId: null,
},
],
},
otherLicensedProducts: [],
meteredProducts: [
{
id: 'metered-1',
name: 'Metered Product',
billingPrices: [
{
interval: null,
tiersMode: null,
tiers: null,
stripePriceId: null,
},
],
type: BillingUsageType.METERED,
prices: [
{
tiersMode: null,
tiers: [],
recurringInterval: SubscriptionInterval.Month,
stripePriceId: null,
},
],
},
],
});
});
});

View File

@ -13,6 +13,7 @@ describe('isStripeValidProductMetadata', () => {
const metadata: Stripe.Metadata = { const metadata: Stripe.Metadata = {
planKey: BillingPlanKey.PRO, planKey: BillingPlanKey.PRO,
priceUsageBased: BillingUsageType.METERED, priceUsageBased: BillingUsageType.METERED,
isBaseProduct: 'true',
}; };
expect(isStripeValidProductMetadata(metadata)).toBe(true); expect(isStripeValidProductMetadata(metadata)).toBe(true);
@ -22,6 +23,7 @@ describe('isStripeValidProductMetadata', () => {
const metadata: Stripe.Metadata = { const metadata: Stripe.Metadata = {
planKey: BillingPlanKey.ENTERPRISE, planKey: BillingPlanKey.ENTERPRISE,
priceUsageBased: BillingUsageType.METERED, priceUsageBased: BillingUsageType.METERED,
isBaseProduct: 'false',
randomKey: 'randomValue', randomKey: 'randomValue',
}; };
@ -32,6 +34,7 @@ describe('isStripeValidProductMetadata', () => {
const metadata: Stripe.Metadata = { const metadata: Stripe.Metadata = {
planKey: 'invalid', planKey: 'invalid',
priceUsageBased: BillingUsageType.METERED, priceUsageBased: BillingUsageType.METERED,
isBaseProduct: 'invalid',
}; };
expect(isStripeValidProductMetadata(metadata)).toBe(false); expect(isStripeValidProductMetadata(metadata)).toBe(false);
@ -41,6 +44,7 @@ describe('isStripeValidProductMetadata', () => {
const metadata: Stripe.Metadata = { const metadata: Stripe.Metadata = {
planKey: BillingPlanKey.PRO, planKey: BillingPlanKey.PRO,
priceUsageBased: 'invalid', priceUsageBased: 'invalid',
isBaseProduct: 'true',
}; };
expect(isStripeValidProductMetadata(metadata)).toBe(false); expect(isStripeValidProductMetadata(metadata)).toBe(false);

View File

@ -2,7 +2,7 @@ import Stripe from 'stripe';
import { BillingMeterEventTimeWindow } from 'src/engine/core-modules/billing/enums/billing-meter-event-time-window.enum'; import { BillingMeterEventTimeWindow } from 'src/engine/core-modules/billing/enums/billing-meter-event-time-window.enum';
import { BillingMeterStatus } from 'src/engine/core-modules/billing/enums/billing-meter-status.enum'; import { BillingMeterStatus } from 'src/engine/core-modules/billing/enums/billing-meter-status.enum';
import { transformStripeMeterDataToMeterRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-meter-data-to-meter-repository-data.util'; import { transformStripeMeterToDatabaseMeter } from 'src/engine/core-modules/billing/utils/transform-stripe-meter-to-database-meter.util';
describe('transformStripeMeterDataToMeterRepositoryData', () => { describe('transformStripeMeterDataToMeterRepositoryData', () => {
it('should return the correct data with customer mapping', () => { it('should return the correct data with customer mapping', () => {
@ -31,7 +31,7 @@ describe('transformStripeMeterDataToMeterRepositoryData', () => {
}, },
}; };
const result = transformStripeMeterDataToMeterRepositoryData(data); const result = transformStripeMeterToDatabaseMeter(data);
expect(result).toEqual({ expect(result).toEqual({
stripeMeterId: 'met_123', stripeMeterId: 'met_123',
@ -74,7 +74,7 @@ describe('transformStripeMeterDataToMeterRepositoryData', () => {
}, },
}; };
const result = transformStripeMeterDataToMeterRepositoryData(data); const result = transformStripeMeterToDatabaseMeter(data);
expect(result).toEqual({ expect(result).toEqual({
stripeMeterId: 'met_1234', stripeMeterId: 'met_1234',

View File

@ -6,8 +6,8 @@ import { BillingPriceTiersMode } from 'src/engine/core-modules/billing/enums/bil
import { BillingPriceType } from 'src/engine/core-modules/billing/enums/billing-price-type.enum'; import { BillingPriceType } from 'src/engine/core-modules/billing/enums/billing-price-type.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'; import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
import { transformStripePriceDataToPriceRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-price-data-to-price-repository-data.util'; import { transformStripePriceToDatabasePrice } from 'src/engine/core-modules/billing/utils/transform-stripe-price-to-database-price.util';
describe('transformStripePriceDataToPriceRepositoryData', () => { describe('transformStripePriceToDatabasePrice', () => {
const createMockPrice = (overrides = {}): Stripe.Price => const createMockPrice = (overrides = {}): Stripe.Price =>
({ ({
id: 'price_123', id: 'price_123',
@ -34,7 +34,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
it('should transform basic price data correctly', () => { it('should transform basic price data correctly', () => {
const mockPrice = createMockPrice(); const mockPrice = createMockPrice();
const result = transformStripePriceDataToPriceRepositoryData(mockPrice); const result = transformStripePriceToDatabasePrice(mockPrice);
expect(result).toEqual({ expect(result).toEqual({
stripePriceId: 'price_123', stripePriceId: 'price_123',
@ -73,7 +73,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
const mockPrice = createMockPrice({ const mockPrice = createMockPrice({
tax_behavior: stripeTaxBehavior as Stripe.Price.TaxBehavior, tax_behavior: stripeTaxBehavior as Stripe.Price.TaxBehavior,
}); });
const result = transformStripePriceDataToPriceRepositoryData(mockPrice); const result = transformStripePriceToDatabasePrice(mockPrice);
expect(result.taxBehavior).toBe(expected); expect(result.taxBehavior).toBe(expected);
}, },
@ -88,7 +88,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
const mockPrice = createMockPrice({ const mockPrice = createMockPrice({
type: stripeType as Stripe.Price.Type, type: stripeType as Stripe.Price.Type,
}); });
const result = transformStripePriceDataToPriceRepositoryData(mockPrice); const result = transformStripePriceToDatabasePrice(mockPrice);
expect(result.type).toBe(expected); expect(result.type).toBe(expected);
}); });
@ -104,7 +104,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
const mockPrice = createMockPrice({ const mockPrice = createMockPrice({
billing_scheme: stripeScheme as Stripe.Price.BillingScheme, billing_scheme: stripeScheme as Stripe.Price.BillingScheme,
}); });
const result = transformStripePriceDataToPriceRepositoryData(mockPrice); const result = transformStripePriceToDatabasePrice(mockPrice);
expect(result.billingScheme).toBe(expected); expect(result.billingScheme).toBe(expected);
}, },
@ -120,7 +120,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
meter: 'meter_123', meter: 'meter_123',
}, },
}); });
const result = transformStripePriceDataToPriceRepositoryData(mockPrice); const result = transformStripePriceToDatabasePrice(mockPrice);
expect(result.stripeMeterId).toBe('meter_123'); expect(result.stripeMeterId).toBe('meter_123');
expect(result.usageType).toBe(BillingUsageType.METERED); expect(result.usageType).toBe(BillingUsageType.METERED);
@ -139,7 +139,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
meter: null, meter: null,
}, },
}); });
const result = transformStripePriceDataToPriceRepositoryData(mockPrice); const result = transformStripePriceToDatabasePrice(mockPrice);
expect(result.interval).toBe(expected); expect(result.interval).toBe(expected);
}); });
@ -162,7 +162,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
tiers: mockTiers, tiers: mockTiers,
tiers_mode: stripeTiersMode as Stripe.Price.TiersMode, tiers_mode: stripeTiersMode as Stripe.Price.TiersMode,
}); });
const result = transformStripePriceDataToPriceRepositoryData(mockPrice); const result = transformStripePriceToDatabasePrice(mockPrice);
expect(result.tiersMode).toBe(expected); expect(result.tiersMode).toBe(expected);
expect(result.tiers).toEqual(mockTiers); expect(result.tiers).toEqual(mockTiers);
@ -179,7 +179,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
const mockPrice = createMockPrice({ const mockPrice = createMockPrice({
transform_quantity: transformQuantity, transform_quantity: transformQuantity,
}); });
const result = transformStripePriceDataToPriceRepositoryData(mockPrice); const result = transformStripePriceToDatabasePrice(mockPrice);
expect(result.transformQuantity).toEqual(transformQuantity); expect(result.transformQuantity).toEqual(transformQuantity);
}); });
@ -192,7 +192,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
}, },
}; };
const mockPrice = createMockPrice({ currency_options: currencyOptions }); const mockPrice = createMockPrice({ currency_options: currencyOptions });
const result = transformStripePriceDataToPriceRepositoryData(mockPrice); const result = transformStripePriceToDatabasePrice(mockPrice);
expect(result.currencyOptions).toEqual(currencyOptions); expect(result.currencyOptions).toEqual(currencyOptions);
}); });
@ -206,7 +206,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
tiers: null, tiers: null,
currency_options: null, currency_options: null,
}); });
const result = transformStripePriceDataToPriceRepositoryData(mockPrice); const result = transformStripePriceToDatabasePrice(mockPrice);
expect(result.nickname).toBeUndefined(); expect(result.nickname).toBeUndefined();
expect(result.unitAmount).toBeUndefined(); expect(result.unitAmount).toBeUndefined();

View File

@ -1,7 +1,7 @@
import Stripe from 'stripe'; import Stripe from 'stripe';
import { transformStripeProductDataToProductRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-product-data-to-product-repository-data.util'; import { transformStripeProductToDatabaseProduct } from 'src/engine/core-modules/billing/utils/transform-stripe-product-to-database-product.util';
describe('transformStripeProductDataToProductRepositoryData', () => { describe('transformStripeProductToDatabaseProduct', () => {
it('should return the correct data', () => { it('should return the correct data', () => {
const data: Stripe.Product = { const data: Stripe.Product = {
id: 'prod_123', id: 'prod_123',
@ -28,7 +28,7 @@ describe('transformStripeProductDataToProductRepositoryData', () => {
metadata: { key: 'value' }, metadata: { key: 'value' },
}; };
const result = transformStripeProductDataToProductRepositoryData(data); const result = transformStripeProductToDatabaseProduct(data);
expect(result).toEqual({ expect(result).toEqual({
stripeProductId: 'prod_123', stripeProductId: 'prod_123',
@ -67,7 +67,7 @@ describe('transformStripeProductDataToProductRepositoryData', () => {
metadata: {}, metadata: {},
}; };
const result = transformStripeProductDataToProductRepositoryData(data); const result = transformStripeProductToDatabaseProduct(data);
expect(result).toEqual({ expect(result).toEqual({
stripeProductId: 'prod_456', stripeProductId: 'prod_456',

View File

@ -0,0 +1,70 @@
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 { BillingPlanOutput } from 'src/engine/core-modules/billing/dtos/outputs/billing-plan.output';
import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-price.entity';
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 { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
import { BillingGetPlanResult } from 'src/engine/core-modules/billing/types/billing-get-plan-result.type';
export const formatBillingDatabaseProductToGraphqlDTO = (
plan: BillingGetPlanResult,
): BillingPlanOutput => {
return {
planKey: plan.planKey,
baseProduct: {
...plan.baseProduct,
type: BillingUsageType.LICENSED,
prices: plan.baseProduct.billingPrices.map(
formatBillingDatabasePriceToLicensedPriceDTO,
),
},
otherLicensedProducts: plan.otherLicensedProducts.map((product) => {
return {
...product,
type: BillingUsageType.LICENSED,
prices: product.billingPrices.map(
formatBillingDatabasePriceToLicensedPriceDTO,
),
};
}),
meteredProducts: plan.meteredProducts.map((product) => {
return {
...product,
type: BillingUsageType.METERED,
prices: product.billingPrices.map(
formatBillingDatabasePriceToMeteredPriceDTO,
),
};
}),
};
};
const formatBillingDatabasePriceToMeteredPriceDTO = (
billingPrice: BillingPrice,
): BillingPriceMeteredDTO => {
return {
tiersMode:
billingPrice?.tiersMode === BillingPriceTiersMode.GRADUATED
? BillingPriceTiersMode.GRADUATED
: null,
tiers:
billingPrice?.tiers?.map((tier) => ({
upTo: tier.up_to,
flatAmount: tier.flat_amount,
unitAmount: tier.unit_amount,
})) ?? [],
recurringInterval: billingPrice?.interval ?? SubscriptionInterval.Month,
stripePriceId: billingPrice?.stripePriceId,
};
};
const formatBillingDatabasePriceToLicensedPriceDTO = (
billingPrice: BillingPrice,
): BillingPriceLicensedDTO => {
return {
recurringInterval: billingPrice?.interval ?? SubscriptionInterval.Month,
unitAmount: billingPrice?.unitAmount ?? 0,
stripePriceId: billingPrice?.stripePriceId,
};
};

View File

@ -12,8 +12,10 @@ export function isStripeValidProductMetadata(
} }
const hasBillingPlanKey = isValidBillingPlanKey(metadata.planKey); const hasBillingPlanKey = isValidBillingPlanKey(metadata.planKey);
const hasPriceUsageBased = isValidPriceUsageBased(metadata.priceUsageBased); const hasPriceUsageBased = isValidPriceUsageBased(metadata.priceUsageBased);
const hasIsBaseProduct =
metadata.isBaseProduct === 'true' || metadata.isBaseProduct === 'false';
return hasBillingPlanKey && hasPriceUsageBased; return hasBillingPlanKey && hasPriceUsageBased && hasIsBaseProduct;
} }
const isValidBillingPlanKey = (planKey?: string) => { const isValidBillingPlanKey = (planKey?: string) => {

View File

@ -1,23 +0,0 @@
import Stripe from 'stripe';
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
export const transformStripeEntitlementUpdatedEventToEntitlementRepositoryData =
(
workspaceId: string,
data: Stripe.EntitlementsActiveEntitlementSummaryUpdatedEvent.Data,
) => {
const stripeCustomerId = data.object.customer;
const activeEntitlementsKeys = data.object.entitlements.data.map(
(entitlement) => entitlement.lookup_key,
);
return Object.values(BillingEntitlementKey).map((key) => {
return {
workspaceId,
key,
value: activeEntitlementsKeys.includes(key),
stripeCustomerId,
};
});
};

View File

@ -3,7 +3,7 @@ import Stripe from 'stripe';
import { BillingMeterEventTimeWindow } from 'src/engine/core-modules/billing/enums/billing-meter-event-time-window.enum'; import { BillingMeterEventTimeWindow } from 'src/engine/core-modules/billing/enums/billing-meter-event-time-window.enum';
import { BillingMeterStatus } from 'src/engine/core-modules/billing/enums/billing-meter-status.enum'; import { BillingMeterStatus } from 'src/engine/core-modules/billing/enums/billing-meter-status.enum';
export const transformStripeMeterDataToMeterRepositoryData = ( export const transformStripeMeterToDatabaseMeter = (
data: Stripe.Billing.Meter, data: Stripe.Billing.Meter,
) => { ) => {
return { return {

View File

@ -7,9 +7,7 @@ import { BillingPriceType } from 'src/engine/core-modules/billing/enums/billing-
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'; import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
export const transformStripePriceDataToPriceRepositoryData = ( export const transformStripePriceToDatabasePrice = (data: Stripe.Price) => {
data: Stripe.Price,
) => {
return { return {
stripePriceId: data.id, stripePriceId: data.id,
active: data.active, active: data.active,

View File

@ -1,13 +1,13 @@
import Stripe from 'stripe'; import Stripe from 'stripe';
export const transformStripeProductDataToProductRepositoryData = ( export const transformStripeProductToDatabaseProduct = (
data: Stripe.Product, data: Stripe.Product,
) => { ) => {
return { return {
stripeProductId: data.id, stripeProductId: data.id,
name: data.name, name: data.name,
active: data.active, active: data.active,
description: data.description, description: data.description ?? '',
images: data.images, images: data.images,
marketingFeatures: data.marketing_features, marketingFeatures: data.marketing_features,
defaultStripePriceId: data.default_price defaultStripePriceId: data.default_price

View File

@ -1,26 +0,0 @@
import Stripe from 'stripe';
export const transformStripeSubscriptionEventToSubscriptionItemRepositoryData =
(
billingSubscriptionId: string,
data:
| Stripe.CustomerSubscriptionUpdatedEvent.Data
| Stripe.CustomerSubscriptionCreatedEvent.Data
| Stripe.CustomerSubscriptionDeletedEvent.Data,
) => {
return data.object.items.data.map((item) => {
return {
billingSubscriptionId,
stripeSubscriptionId: data.object.id,
stripeProductId: String(item.price.product),
stripePriceId: item.price.id,
stripeSubscriptionItemId: item.id,
quantity: item.quantity,
metadata: item.metadata,
billingThresholds:
item.billing_thresholds === null
? undefined
: item.billing_thresholds,
};
});
};

View File

@ -10,7 +10,7 @@ import {
} from 'src/engine/core-modules/billing/billing.exception'; } from 'src/engine/core-modules/billing/billing.exception';
import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity'; import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { transformStripeEntitlementUpdatedEventToEntitlementRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-entitlement-updated-event-to-entitlement-repository-data.util'; import { transformStripeEntitlementUpdatedEventToDatabaseEntitlement } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-entitlement-updated-event-to-database-entitlement.util';
@Injectable() @Injectable()
export class BillingWebhookEntitlementService { export class BillingWebhookEntitlementService {
protected readonly logger = new Logger(BillingWebhookEntitlementService.name); protected readonly logger = new Logger(BillingWebhookEntitlementService.name);
@ -39,7 +39,7 @@ export class BillingWebhookEntitlementService {
const workspaceId = billingSubscription.workspaceId; const workspaceId = billingSubscription.workspaceId;
await this.billingEntitlementRepository.upsert( await this.billingEntitlementRepository.upsert(
transformStripeEntitlementUpdatedEventToEntitlementRepositoryData( transformStripeEntitlementUpdatedEventToDatabaseEntitlement(
workspaceId, workspaceId,
data, data,
), ),

View File

@ -12,8 +12,9 @@ import { BillingMeter } from 'src/engine/core-modules/billing/entities/billing-m
import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-price.entity'; import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-price.entity';
import { BillingProduct } from 'src/engine/core-modules/billing/entities/billing-product.entity'; import { BillingProduct } from 'src/engine/core-modules/billing/entities/billing-product.entity';
import { StripeBillingMeterService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-meter.service'; import { StripeBillingMeterService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-meter.service';
import { transformStripeMeterDataToMeterRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-meter-data-to-meter-repository-data.util'; import { transformStripeMeterToDatabaseMeter } from 'src/engine/core-modules/billing/utils/transform-stripe-meter-to-database-meter.util';
import { transformStripePriceEventToPriceRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-price-event-to-price-repository-data.util'; import { transformStripePriceEventToDatabasePrice } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-price-event-to-database-price.util';
@Injectable() @Injectable()
export class BillingWebhookPriceService { export class BillingWebhookPriceService {
protected readonly logger = new Logger(BillingWebhookPriceService.name); protected readonly logger = new Logger(BillingWebhookPriceService.name);
@ -48,7 +49,7 @@ export class BillingWebhookPriceService {
const meterData = await this.stripeBillingMeterService.getMeter(meterId); const meterData = await this.stripeBillingMeterService.getMeter(meterId);
await this.billingMeterRepository.upsert( await this.billingMeterRepository.upsert(
transformStripeMeterDataToMeterRepositoryData(meterData), transformStripeMeterToDatabaseMeter(meterData),
{ {
conflictPaths: ['stripeMeterId'], conflictPaths: ['stripeMeterId'],
skipUpdateIfNoValuesChanged: true, skipUpdateIfNoValuesChanged: true,
@ -57,7 +58,7 @@ export class BillingWebhookPriceService {
} }
await this.billingPriceRepository.upsert( await this.billingPriceRepository.upsert(
transformStripePriceEventToPriceRepositoryData(data), transformStripePriceEventToDatabasePrice(data),
{ {
conflictPaths: ['stripePriceId'], conflictPaths: ['stripePriceId'],
skipUpdateIfNoValuesChanged: true, skipUpdateIfNoValuesChanged: true,

View File

@ -9,7 +9,7 @@ import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-pl
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.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 { 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 { isStripeValidProductMetadata } from 'src/engine/core-modules/billing/utils/is-stripe-valid-product-metadata.util';
import { transformStripeProductEventToProductRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-product-event-to-product-repository-data.util'; import { transformStripeProductEventToDatabaseProduct } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-product-event-to-database-product.util';
@Injectable() @Injectable()
export class BillingWebhookProductService { export class BillingWebhookProductService {
protected readonly logger = new Logger(BillingWebhookProductService.name); protected readonly logger = new Logger(BillingWebhookProductService.name);
@ -24,10 +24,10 @@ export class BillingWebhookProductService {
const metadata = data.object.metadata; const metadata = data.object.metadata;
const productRepositoryData = isStripeValidProductMetadata(metadata) const productRepositoryData = isStripeValidProductMetadata(metadata)
? { ? {
...transformStripeProductEventToProductRepositoryData(data), ...transformStripeProductEventToDatabaseProduct(data),
metadata, metadata,
} }
: transformStripeProductEventToProductRepositoryData(data); : transformStripeProductEventToDatabaseProduct(data);
await this.billingProductRepository.upsert(productRepositoryData, { await this.billingProductRepository.upsert(productRepositoryData, {
conflictPaths: ['stripeProductId'], conflictPaths: ['stripeProductId'],

View File

@ -10,9 +10,9 @@ import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entitie
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum'; import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
import { StripeCustomerService } from 'src/engine/core-modules/billing/stripe/services/stripe-customer.service'; import { StripeCustomerService } from 'src/engine/core-modules/billing/stripe/services/stripe-customer.service';
import { transformStripeSubscriptionEventToCustomerRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-customer-repository-data.util'; import { transformStripeSubscriptionEventToDatabaseCustomer } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-customer.util';
import { transformStripeSubscriptionEventToSubscriptionItemRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-item-repository-data.util'; import { transformStripeSubscriptionEventToDatabaseSubscriptionItem } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-subscription-item.util';
import { transformStripeSubscriptionEventToSubscriptionRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-repository-data.util'; import { transformStripeSubscriptionEventToDatabaseSubscription } 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'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Injectable() @Injectable()
export class BillingWebhookSubscriptionService { export class BillingWebhookSubscriptionService {
@ -47,10 +47,7 @@ export class BillingWebhookSubscriptionService {
} }
await this.billingCustomerRepository.upsert( await this.billingCustomerRepository.upsert(
transformStripeSubscriptionEventToCustomerRepositoryData( transformStripeSubscriptionEventToDatabaseCustomer(workspaceId, data),
workspaceId,
data,
),
{ {
conflictPaths: ['workspaceId'], conflictPaths: ['workspaceId'],
skipUpdateIfNoValuesChanged: true, skipUpdateIfNoValuesChanged: true,
@ -58,10 +55,7 @@ export class BillingWebhookSubscriptionService {
); );
await this.billingSubscriptionRepository.upsert( await this.billingSubscriptionRepository.upsert(
transformStripeSubscriptionEventToSubscriptionRepositoryData( transformStripeSubscriptionEventToDatabaseSubscription(workspaceId, data),
workspaceId,
data,
),
{ {
conflictPaths: ['stripeSubscriptionId'], conflictPaths: ['stripeSubscriptionId'],
skipUpdateIfNoValuesChanged: true, skipUpdateIfNoValuesChanged: true,
@ -74,7 +68,7 @@ export class BillingWebhookSubscriptionService {
}); });
await this.billingSubscriptionItemRepository.upsert( await this.billingSubscriptionItemRepository.upsert(
transformStripeSubscriptionEventToSubscriptionItemRepositoryData( transformStripeSubscriptionEventToDatabaseSubscriptionItem(
billingSubscription.id, billingSubscription.id,
data, data,
), ),

View File

@ -1,9 +1,9 @@
import Stripe from 'stripe'; import Stripe from 'stripe';
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum'; import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
import { transformStripeEntitlementUpdatedEventToEntitlementRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-entitlement-updated-event-to-entitlement-repository-data.util'; import { transformStripeEntitlementUpdatedEventToDatabaseEntitlement } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-entitlement-updated-event-to-database-entitlement.util';
describe('transformStripeEntitlementUpdatedEventToEntitlementRepositoryData', () => { describe('transformStripeEntitlementUpdatedEventToDatabaseEntitlement', () => {
it('should return the SSO key with true value', () => { it('should return the SSO key with true value', () => {
const data: Stripe.EntitlementsActiveEntitlementSummaryUpdatedEvent.Data = { const data: Stripe.EntitlementsActiveEntitlementSummaryUpdatedEvent.Data = {
object: { object: {
@ -27,11 +27,10 @@ describe('transformStripeEntitlementUpdatedEventToEntitlementRepositoryData', ()
}, },
}; };
const result = const result = transformStripeEntitlementUpdatedEventToDatabaseEntitlement(
transformStripeEntitlementUpdatedEventToEntitlementRepositoryData( 'workspaceId',
'workspaceId', data,
data, );
);
expect(result).toEqual([ expect(result).toEqual([
{ {
@ -66,11 +65,10 @@ describe('transformStripeEntitlementUpdatedEventToEntitlementRepositoryData', ()
}, },
}; };
const result = const result = transformStripeEntitlementUpdatedEventToDatabaseEntitlement(
transformStripeEntitlementUpdatedEventToEntitlementRepositoryData( 'workspaceId',
'workspaceId', data,
data, );
);
expect(result).toEqual([ expect(result).toEqual([
{ {

View File

@ -4,9 +4,9 @@ import { BillingPriceTiersMode } from 'src/engine/core-modules/billing/enums/bil
import { BillingPriceType } from 'src/engine/core-modules/billing/enums/billing-price-type.enum'; import { BillingPriceType } from 'src/engine/core-modules/billing/enums/billing-price-type.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'; import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
import { transformStripePriceEventToPriceRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-price-event-to-price-repository-data.util'; import { transformStripePriceEventToDatabasePrice } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-price-event-to-database-price.util';
describe('transformStripePriceEventToPriceRepositoryData', () => { describe('transformStripePriceEventToDatabasePrice', () => {
const createMockPriceData = (overrides = {}) => ({ const createMockPriceData = (overrides = {}) => ({
object: { object: {
id: 'price_123', id: 'price_123',
@ -34,9 +34,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
it('should transform basic price data correctly', () => { it('should transform basic price data correctly', () => {
const mockData = createMockPriceData(); const mockData = createMockPriceData();
const result = transformStripePriceEventToPriceRepositoryData( const result = transformStripePriceEventToDatabasePrice(mockData as any);
mockData as any,
);
expect(result).toEqual({ expect(result).toEqual({
stripePriceId: 'price_123', stripePriceId: 'price_123',
@ -74,9 +72,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
const mockData = createMockPriceData({ const mockData = createMockPriceData({
tax_behavior: stripeTaxBehavior, tax_behavior: stripeTaxBehavior,
}); });
const result = transformStripePriceEventToPriceRepositoryData( const result = transformStripePriceEventToDatabasePrice(mockData as any);
mockData as any,
);
expect(result.taxBehavior).toBe(expectedTaxBehavior); expect(result.taxBehavior).toBe(expectedTaxBehavior);
}); });
@ -90,9 +86,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
priceTypes.forEach(([stripeType, expectedType]) => { priceTypes.forEach(([stripeType, expectedType]) => {
const mockData = createMockPriceData({ type: stripeType }); const mockData = createMockPriceData({ type: stripeType });
const result = transformStripePriceEventToPriceRepositoryData( const result = transformStripePriceEventToDatabasePrice(mockData as any);
mockData as any,
);
expect(result.type).toBe(expectedType); expect(result.type).toBe(expectedType);
}); });
@ -106,9 +100,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
billingSchemes.forEach(([stripeScheme, expectedScheme]) => { billingSchemes.forEach(([stripeScheme, expectedScheme]) => {
const mockData = createMockPriceData({ billing_scheme: stripeScheme }); const mockData = createMockPriceData({ billing_scheme: stripeScheme });
const result = transformStripePriceEventToPriceRepositoryData( const result = transformStripePriceEventToDatabasePrice(mockData as any);
mockData as any,
);
expect(result.billingScheme).toBe(expectedScheme); expect(result.billingScheme).toBe(expectedScheme);
}); });
@ -124,9 +116,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
const mockData = createMockPriceData({ const mockData = createMockPriceData({
recurring: { usage_type: stripeUsageType, interval: 'month' }, recurring: { usage_type: stripeUsageType, interval: 'month' },
}); });
const result = transformStripePriceEventToPriceRepositoryData( const result = transformStripePriceEventToDatabasePrice(mockData as any);
mockData as any,
);
expect(result.usageType).toBe(expectedUsageType); expect(result.usageType).toBe(expectedUsageType);
}); });
@ -140,9 +130,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
tiersModes.forEach(([stripeTiersMode, expectedTiersMode]) => { tiersModes.forEach(([stripeTiersMode, expectedTiersMode]) => {
const mockData = createMockPriceData({ tiers_mode: stripeTiersMode }); const mockData = createMockPriceData({ tiers_mode: stripeTiersMode });
const result = transformStripePriceEventToPriceRepositoryData( const result = transformStripePriceEventToDatabasePrice(mockData as any);
mockData as any,
);
expect(result.tiersMode).toBe(expectedTiersMode); expect(result.tiersMode).toBe(expectedTiersMode);
}); });
@ -160,9 +148,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
const mockData = createMockPriceData({ const mockData = createMockPriceData({
recurring: { usage_type: 'licensed', interval: stripeInterval }, recurring: { usage_type: 'licensed', interval: stripeInterval },
}); });
const result = transformStripePriceEventToPriceRepositoryData( const result = transformStripePriceEventToDatabasePrice(mockData as any);
mockData as any,
);
expect(result.interval).toBe(expectedInterval); expect(result.interval).toBe(expectedInterval);
}); });
@ -180,9 +166,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
tiers_mode: 'graduated', tiers_mode: 'graduated',
}); });
const result = transformStripePriceEventToPriceRepositoryData( const result = transformStripePriceEventToDatabasePrice(mockData as any);
mockData as any,
);
expect(result.billingScheme).toBe(BillingPriceBillingScheme.TIERED); expect(result.billingScheme).toBe(BillingPriceBillingScheme.TIERED);
expect(result.tiers).toEqual(mockTiers); expect(result.tiers).toEqual(mockTiers);
@ -204,9 +188,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
transform_quantity: mockTransformQuantity, transform_quantity: mockTransformQuantity,
}); });
const result = transformStripePriceEventToPriceRepositoryData( const result = transformStripePriceEventToDatabasePrice(mockData as any);
mockData as any,
);
expect(result.stripeMeterId).toBe('meter_123'); expect(result.stripeMeterId).toBe('meter_123');
expect(result.usageType).toBe(BillingUsageType.METERED); expect(result.usageType).toBe(BillingUsageType.METERED);
@ -225,9 +207,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
currency_options: mockCurrencyOptions, currency_options: mockCurrencyOptions,
}); });
const result = transformStripePriceEventToPriceRepositoryData( const result = transformStripePriceEventToDatabasePrice(mockData as any);
mockData as any,
);
expect(result.currencyOptions).toEqual(mockCurrencyOptions); expect(result.currencyOptions).toEqual(mockCurrencyOptions);
}); });

View File

@ -1,8 +1,8 @@
import Stripe from 'stripe'; import Stripe from 'stripe';
import { transformStripeProductEventToProductRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-product-event-to-product-repository-data.util'; import { transformStripeProductEventToDatabaseProduct } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-product-event-to-database-product.util';
describe('transformStripeProductEventToProductRepositoryData', () => { describe('transformStripeProductEventToDatabaseProduct', () => {
it('should return the correct data', () => { it('should return the correct data', () => {
const data: Stripe.ProductCreatedEvent.Data = { const data: Stripe.ProductCreatedEvent.Data = {
object: { object: {
@ -31,7 +31,7 @@ describe('transformStripeProductEventToProductRepositoryData', () => {
}, },
}; };
const result = transformStripeProductEventToProductRepositoryData(data); const result = transformStripeProductEventToDatabaseProduct(data);
expect(result).toEqual({ expect(result).toEqual({
stripeProductId: 'prod_123', stripeProductId: 'prod_123',
@ -71,7 +71,7 @@ describe('transformStripeProductEventToProductRepositoryData', () => {
}, },
}; };
const result = transformStripeProductEventToProductRepositoryData(data); const result = transformStripeProductEventToDatabaseProduct(data);
expect(result).toEqual({ expect(result).toEqual({
stripeProductId: 'prod_456', stripeProductId: 'prod_456',

View File

@ -1,6 +1,5 @@
import { transformStripeSubscriptionEventToCustomerRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-customer-repository-data.util'; import { transformStripeSubscriptionEventToDatabaseCustomer } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-customer.util';
describe('transformStripeSubscriptionEventToDatabaseCustomer', () => {
describe('transformStripeSubscriptionEventToCustomerRepositoryData', () => {
const mockWorkspaceId = 'workspace_123'; const mockWorkspaceId = 'workspace_123';
const mockTimestamp = 1672531200; // 2023-01-01 00:00:00 UTC const mockTimestamp = 1672531200; // 2023-01-01 00:00:00 UTC
@ -38,7 +37,7 @@ describe('transformStripeSubscriptionEventToCustomerRepositoryData', () => {
it('should transform basic customer data correctly', () => { it('should transform basic customer data correctly', () => {
const mockData = createMockSubscriptionData('cus_123'); const mockData = createMockSubscriptionData('cus_123');
const result = transformStripeSubscriptionEventToCustomerRepositoryData( const result = transformStripeSubscriptionEventToDatabaseCustomer(
mockWorkspaceId, mockWorkspaceId,
mockData as any, mockData as any,
); );
@ -54,7 +53,7 @@ describe('transformStripeSubscriptionEventToCustomerRepositoryData', () => {
// Test with different event types (they should all transform the same way) // Test with different event types (they should all transform the same way)
['updated', 'created', 'deleted'].forEach(() => { ['updated', 'created', 'deleted'].forEach(() => {
const result = transformStripeSubscriptionEventToCustomerRepositoryData( const result = transformStripeSubscriptionEventToDatabaseCustomer(
mockWorkspaceId, mockWorkspaceId,
mockData as any, mockData as any,
); );
@ -71,7 +70,7 @@ describe('transformStripeSubscriptionEventToCustomerRepositoryData', () => {
const testWorkspaces = ['workspace_1', 'workspace_2', 'workspace_abc']; const testWorkspaces = ['workspace_1', 'workspace_2', 'workspace_abc'];
testWorkspaces.forEach((testWorkspaceId) => { testWorkspaces.forEach((testWorkspaceId) => {
const result = transformStripeSubscriptionEventToCustomerRepositoryData( const result = transformStripeSubscriptionEventToDatabaseCustomer(
testWorkspaceId, testWorkspaceId,
mockData as any, mockData as any,
); );

View File

@ -1,8 +1,8 @@
import { BillingSubscriptionCollectionMethod } from 'src/engine/core-modules/billing/enums/billing-subscription-collection-method.enum'; import { BillingSubscriptionCollectionMethod } from 'src/engine/core-modules/billing/enums/billing-subscription-collection-method.enum';
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum'; import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
import { transformStripeSubscriptionEventToSubscriptionRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-repository-data.util'; import { transformStripeSubscriptionEventToDatabaseSubscription } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-subscription.util';
describe('transformStripeSubscriptionEventToSubscriptionRepositoryData', () => { describe('transformStripeSubscriptionEventToDatabaseSubscription', () => {
const mockWorkspaceId = 'workspace-123'; const mockWorkspaceId = 'workspace-123';
const mockTimestamp = 1672531200; // 2023-01-01 00:00:00 UTC const mockTimestamp = 1672531200; // 2023-01-01 00:00:00 UTC
@ -39,7 +39,7 @@ describe('transformStripeSubscriptionEventToSubscriptionRepositoryData', () => {
it('should transform basic subscription data correctly', () => { it('should transform basic subscription data correctly', () => {
const mockData = createMockSubscriptionData(); const mockData = createMockSubscriptionData();
const result = transformStripeSubscriptionEventToSubscriptionRepositoryData( const result = transformStripeSubscriptionEventToDatabaseSubscription(
mockWorkspaceId, mockWorkspaceId,
mockData as any, mockData as any,
); );
@ -83,11 +83,10 @@ describe('transformStripeSubscriptionEventToSubscriptionRepositoryData', () => {
const mockData = createMockSubscriptionData({ const mockData = createMockSubscriptionData({
status: stripeStatus, status: stripeStatus,
}); });
const result = const result = transformStripeSubscriptionEventToDatabaseSubscription(
transformStripeSubscriptionEventToSubscriptionRepositoryData( mockWorkspaceId,
mockWorkspaceId, mockData as any,
mockData as any, );
);
expect(result.status).toBe(expectedStatus); expect(result.status).toBe(expectedStatus);
}); });
@ -102,7 +101,7 @@ describe('transformStripeSubscriptionEventToSubscriptionRepositoryData', () => {
trial_end: trialEnd, trial_end: trialEnd,
}); });
const result = transformStripeSubscriptionEventToSubscriptionRepositoryData( const result = transformStripeSubscriptionEventToDatabaseSubscription(
mockWorkspaceId, mockWorkspaceId,
mockData as any, mockData as any,
); );
@ -125,7 +124,7 @@ describe('transformStripeSubscriptionEventToSubscriptionRepositoryData', () => {
}, },
}); });
const result = transformStripeSubscriptionEventToSubscriptionRepositoryData( const result = transformStripeSubscriptionEventToDatabaseSubscription(
mockWorkspaceId, mockWorkspaceId,
mockData as any, mockData as any,
); );
@ -148,7 +147,7 @@ describe('transformStripeSubscriptionEventToSubscriptionRepositoryData', () => {
}, },
}); });
const result = transformStripeSubscriptionEventToSubscriptionRepositoryData( const result = transformStripeSubscriptionEventToDatabaseSubscription(
mockWorkspaceId, mockWorkspaceId,
mockData as any, mockData as any,
); );
@ -172,11 +171,10 @@ describe('transformStripeSubscriptionEventToSubscriptionRepositoryData', () => {
const mockData = createMockSubscriptionData({ const mockData = createMockSubscriptionData({
collection_method: stripeMethod, collection_method: stripeMethod,
}); });
const result = const result = transformStripeSubscriptionEventToDatabaseSubscription(
transformStripeSubscriptionEventToSubscriptionRepositoryData( mockWorkspaceId,
mockWorkspaceId, mockData as any,
mockData as any, );
);
expect(result.collectionMethod).toBe(expectedMethod); expect(result.collectionMethod).toBe(expectedMethod);
}); });
@ -187,7 +185,7 @@ describe('transformStripeSubscriptionEventToSubscriptionRepositoryData', () => {
currency: 'eur', currency: 'eur',
}); });
const result = transformStripeSubscriptionEventToSubscriptionRepositoryData( const result = transformStripeSubscriptionEventToDatabaseSubscription(
mockWorkspaceId, mockWorkspaceId,
mockData as any, mockData as any,
); );

View File

@ -0,0 +1,22 @@
import Stripe from 'stripe';
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
export const transformStripeEntitlementUpdatedEventToDatabaseEntitlement = (
workspaceId: string,
data: Stripe.EntitlementsActiveEntitlementSummaryUpdatedEvent.Data,
) => {
const stripeCustomerId = data.object.customer;
const activeEntitlementsKeys = data.object.entitlements.data.map(
(entitlement) => entitlement.lookup_key,
);
return Object.values(BillingEntitlementKey).map((key) => {
return {
workspaceId,
key,
value: activeEntitlementsKeys.includes(key),
stripeCustomerId,
};
});
};

View File

@ -7,7 +7,7 @@ import { BillingPriceType } from 'src/engine/core-modules/billing/enums/billing-
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'; import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
export const transformStripePriceEventToPriceRepositoryData = ( export const transformStripePriceEventToDatabasePrice = (
data: Stripe.PriceCreatedEvent.Data | Stripe.PriceUpdatedEvent.Data, data: Stripe.PriceCreatedEvent.Data | Stripe.PriceUpdatedEvent.Data,
) => { ) => {
return { return {

View File

@ -1,13 +1,13 @@
import Stripe from 'stripe'; import Stripe from 'stripe';
export const transformStripeProductEventToProductRepositoryData = ( export const transformStripeProductEventToDatabaseProduct = (
data: Stripe.ProductUpdatedEvent.Data | Stripe.ProductCreatedEvent.Data, data: Stripe.ProductUpdatedEvent.Data | Stripe.ProductCreatedEvent.Data,
) => { ) => {
return { return {
stripeProductId: data.object.id, stripeProductId: data.object.id,
name: data.object.name, name: data.object.name,
active: data.object.active, active: data.object.active,
description: data.object.description, description: data.object.description ?? '',
images: data.object.images, images: data.object.images,
marketingFeatures: data.object.marketing_features, marketingFeatures: data.object.marketing_features,
defaultStripePriceId: data.object.default_price defaultStripePriceId: data.object.default_price

View File

@ -1,6 +1,6 @@
import Stripe from 'stripe'; import Stripe from 'stripe';
export const transformStripeSubscriptionEventToCustomerRepositoryData = ( export const transformStripeSubscriptionEventToDatabaseCustomer = (
workspaceId: string, workspaceId: string,
data: data:
| Stripe.CustomerSubscriptionUpdatedEvent.Data | Stripe.CustomerSubscriptionUpdatedEvent.Data

View File

@ -0,0 +1,23 @@
import Stripe from 'stripe';
export const transformStripeSubscriptionEventToDatabaseSubscriptionItem = (
billingSubscriptionId: string,
data:
| Stripe.CustomerSubscriptionUpdatedEvent.Data
| Stripe.CustomerSubscriptionCreatedEvent.Data
| Stripe.CustomerSubscriptionDeletedEvent.Data,
) => {
return data.object.items.data.map((item) => {
return {
billingSubscriptionId,
stripeSubscriptionId: data.object.id,
stripeProductId: String(item.price.product),
stripePriceId: item.price.id,
stripeSubscriptionItemId: item.id,
quantity: item.quantity,
metadata: item.metadata,
billingThresholds:
item.billing_thresholds === null ? undefined : item.billing_thresholds,
};
});
};

View File

@ -3,7 +3,7 @@ import Stripe from 'stripe';
import { BillingSubscriptionCollectionMethod } from 'src/engine/core-modules/billing/enums/billing-subscription-collection-method.enum'; import { BillingSubscriptionCollectionMethod } from 'src/engine/core-modules/billing/enums/billing-subscription-collection-method.enum';
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum'; import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
export const transformStripeSubscriptionEventToSubscriptionRepositoryData = ( export const transformStripeSubscriptionEventToDatabaseSubscription = (
workspaceId: string, workspaceId: string,
data: data:
| Stripe.CustomerSubscriptionUpdatedEvent.Data | Stripe.CustomerSubscriptionUpdatedEvent.Data

View File

@ -1,6 +1,6 @@
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { TrialPeriodDTO } from 'src/engine/core-modules/billing/dto/trial-period.dto'; import { BillingTrialPeriodDTO } from 'src/engine/core-modules/billing/dtos/billing-trial-period.dto';
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces'; import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
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 { AuthProviders } from 'src/engine/core-modules/workspace/dtos/public-workspace-data-output'; import { AuthProviders } from 'src/engine/core-modules/workspace/dtos/public-workspace-data-output';
@ -17,8 +17,8 @@ class Billing {
@Field(() => String, { nullable: true }) @Field(() => String, { nullable: true })
billingUrl?: string; billingUrl?: string;
@Field(() => [TrialPeriodDTO]) @Field(() => [BillingTrialPeriodDTO])
trialPeriods: TrialPeriodDTO[]; trialPeriods: BillingTrialPeriodDTO[];
} }
@ObjectType() @ObjectType()

View File

@ -15,4 +15,5 @@ export enum FeatureFlagKey {
IsCommandMenuV2Enabled = 'IS_COMMAND_MENU_V2_ENABLED', IsCommandMenuV2Enabled = 'IS_COMMAND_MENU_V2_ENABLED',
IsJsonFilterEnabled = 'IS_JSON_FILTER_ENABLED', IsJsonFilterEnabled = 'IS_JSON_FILTER_ENABLED',
IsLocalizationEnabled = 'IS_LOCALIZATION_ENABLED', IsLocalizationEnabled = 'IS_LOCALIZATION_ENABLED',
IsBillingPlansEnabled = 'IS_BILLING_PLANS_ENABLED',
} }