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,
value: true,
},
{
key: FeatureFlagKey.IsBillingPlansEnabled,
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IsGmailSendEmailScopeEnabled,
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 { 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 { 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 { 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')
@UseFilters(BillingRestApiExceptionFilter)
export class BillingController {

View File

@ -12,5 +12,6 @@ export class BillingException extends CustomException {
export enum BillingExceptionCode {
BILLING_CUSTOMER_NOT_FOUND = 'BILLING_CUSTOMER_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',
}

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 { 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 { BillingPlanService } from 'src/engine/core-modules/billing/services/billing-plan.service';
import { BillingPortalWorkspaceService } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { 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 { 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 { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
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,
BillingPortalWorkspaceService,
BillingResolver,
BillingPlanService,
BillingWorkspaceMemberListener,
BillingService,
BillingWebhookProductService,

View File

@ -1,16 +1,24 @@
import { UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { BillingSessionInput } from 'src/engine/core-modules/billing/dto/billing-session.input';
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 { ProductInput } from 'src/engine/core-modules/billing/dto/product.input';
import { SessionEntity } from 'src/engine/core-modules/billing/dto/session.entity';
import { UpdateBillingEntity } from 'src/engine/core-modules/billing/dto/update-billing.entity';
import { GraphQLError } from 'graphql';
import { BillingCheckoutSessionInput } from 'src/engine/core-modules/billing/dtos/inputs/billing-checkout-session.input';
import { BillingProductInput } from 'src/engine/core-modules/billing/dtos/inputs/billing-product.input';
import { BillingSessionInput } from 'src/engine/core-modules/billing/dtos/inputs/billing-session.input';
import { 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 { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
import { BillingPlanService } from 'src/engine/core-modules/billing/services/billing-plan.service';
import { BillingPortalWorkspaceService } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service';
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
import { 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 { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
@ -24,20 +32,26 @@ export class BillingResolver {
private readonly billingSubscriptionService: BillingSubscriptionService,
private readonly billingPortalWorkspaceService: BillingPortalWorkspaceService,
private readonly stripePriceService: StripePriceService,
private readonly billingPlanService: BillingPlanService,
private readonly featureFlagService: FeatureFlagService,
) {}
@Query(() => ProductPricesEntity)
async getProductPrices(@Args() { product }: ProductInput) {
@Query(() => BillingProductPricesOutput)
@UseGuards(WorkspaceAuthGuard)
async getProductPrices(
@AuthWorkspace() workspace: Workspace,
@Args() { product }: BillingProductInput,
) {
const productPrices =
await this.stripePriceService.getStripePrices(product);
return {
totalNumberOfPrices: productPrices.length,
productPrices: productPrices,
productPrices,
};
}
@Query(() => SessionEntity)
@Query(() => BillingSessionOutput)
@UseGuards(WorkspaceAuthGuard)
async billingPortalSession(
@AuthWorkspace() workspace: Workspace,
@ -51,7 +65,7 @@ export class BillingResolver {
};
}
@Mutation(() => SessionEntity)
@Mutation(() => BillingSessionOutput)
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
async checkoutSession(
@AuthWorkspace() workspace: Workspace,
@ -62,15 +76,37 @@ export class BillingResolver {
successUrlPath,
plan,
requirePaymentMethod,
}: CheckoutSessionInput,
}: BillingCheckoutSessionInput,
) {
const productPrice = await this.stripePriceService.getStripePrice(
AvailableProduct.BasePlan,
recurringInterval,
);
const isBillingPlansEnabled =
await this.featureFlagService.isFeatureEnabled(
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) {
throw new Error(
throw new GraphQLError(
'Product price not found for the given recurring interval',
);
}
@ -87,11 +123,19 @@ export class BillingResolver {
};
}
@Mutation(() => UpdateBillingEntity)
@Mutation(() => BillingUpdateOutput)
@UseGuards(WorkspaceAuthGuard)
async updateBillingSubscription(@AuthWorkspace() workspace: Workspace) {
await this.billingSubscriptionService.applyBillingSubscription(workspace);
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 { 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 { transformStripeMeterDataToMeterRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-meter-data-to-meter-repository-data.util';
import { transformStripePriceDataToPriceRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-price-data-to-price-repository-data.util';
import { transformStripeProductDataToProductRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-product-data-to-product-repository-data.util';
import { transformStripeMeterToDatabaseMeter } from 'src/engine/core-modules/billing/utils/transform-stripe-meter-to-database-meter.util';
import { transformStripePriceToDatabasePrice } from 'src/engine/core-modules/billing/utils/transform-stripe-price-to-database-price.util';
import { transformStripeProductToDatabaseProduct } from 'src/engine/core-modules/billing/utils/transform-stripe-product-to-database-product.util';
@Command({
name: 'billing:sync-plans-data',
description:
@ -47,7 +47,7 @@ export class BillingSyncPlansDataCommand extends BaseCommandRunner {
try {
if (!options.dryRun) {
await this.billingMeterRepository.upsert(
transformStripeMeterDataToMeterRepositoryData(meter),
transformStripeMeterToDatabaseMeter(meter),
{
conflictPaths: ['stripeMeterId'],
},
@ -67,7 +67,7 @@ export class BillingSyncPlansDataCommand extends BaseCommandRunner {
try {
if (!options.dryRun) {
await this.billingProductRepository.upsert(
transformStripeProductDataToProductRepositoryData(product),
transformStripeProductToDatabaseProduct(product),
{
conflictPaths: ['stripeProductId'],
},
@ -148,9 +148,7 @@ export class BillingSyncPlansDataCommand extends BaseCommandRunner {
options,
);
const transformedPrices = billingPrices.flatMap((prices) =>
prices.map((price) =>
transformStripePriceDataToPriceRepositoryData(price),
),
prices.map((price) => transformStripePriceToDatabasePrice(price)),
);
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';
@ObjectType()
export class ProductPriceEntity {
export class BillingProductPriceDTO {
@Field(() => SubscriptionInterval)
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';
@ObjectType()
export class TrialPeriodDTO {
export class BillingTrialPeriodDTO {
@Field(() => Number)
@Min(0)
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';
@ArgsType()
export class CheckoutSessionInput {
export class BillingCheckoutSessionInput {
@Field(() => SubscriptionInterval)
@IsEnum(SubscriptionInterval)
@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';
@ArgsType()
export class ProductInput {
export class BillingProductInput {
@Field(() => String)
@IsString()
@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';
@ObjectType()
export class SessionEntity {
export class BillingSessionOutput {
@Field(() => String, { nullable: true })
url: string;
}

View File

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

View File

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

View File

@ -1,4 +1,10 @@
import { registerEnumType } from '@nestjs/graphql';
export enum BillingPriceTiersMode {
GRADUATED = 'GRADUATED',
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 { 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 { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum';
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
import { BillingPlanService } from 'src/engine/core-modules/billing/services/billing-plan.service';
import { StripePriceService } from 'src/engine/core-modules/billing/stripe/services/stripe-price.service';
import { StripeSubscriptionItemService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service';
import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Injectable()
export class BillingSubscriptionService {
protected readonly logger = new Logger(BillingSubscriptionService.name);
@ -25,7 +32,9 @@ export class BillingSubscriptionService {
private readonly stripeSubscriptionService: StripeSubscriptionService,
private readonly stripePriceService: StripePriceService,
private readonly stripeSubscriptionItemService: StripeSubscriptionItemService,
private readonly billingPlanService: BillingPlanService,
private readonly environmentService: EnvironmentService,
private readonly featureFlagService: FeatureFlagService,
@InjectRepository(BillingEntitlement, 'core')
private readonly billingEntitlementRepository: Repository<BillingEntitlement>,
@InjectRepository(BillingSubscription, 'core')
@ -56,19 +65,37 @@ export class BillingSubscriptionService {
'BILLING_STRIPE_BASE_PLAN_PRODUCT_ID',
),
) {
const isBillingPlansEnabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsBillingPlansEnabled,
workspaceId,
);
const billingSubscription = await this.getCurrentBillingSubscriptionOrThrow(
{ workspaceId },
);
const getStripeProductId = isBillingPlansEnabled
? (await this.billingPlanService.getPlanBaseProduct(BillingPlanKey.PRO))
?.stripeProductId
: stripeProductId;
if (!getStripeProductId) {
throw new BillingException(
'Base product not found',
BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND,
);
}
const billingSubscriptionItem =
billingSubscription.billingSubscriptionItems.filter(
(billingSubscriptionItem) =>
billingSubscriptionItem.stripeProductId === stripeProductId,
billingSubscriptionItem.stripeProductId === getStripeProductId,
)?.[0];
if (!billingSubscriptionItem) {
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(
{ workspaceId: workspace.id },
);
const isBillingPlansEnabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IsBillingPlansEnabled,
workspace.id,
);
const newInterval =
billingSubscription?.interval === SubscriptionInterval.Year
? SubscriptionInterval.Month
@ -136,10 +167,29 @@ export class BillingSubscriptionService {
const billingSubscriptionItem =
await this.getCurrentBillingSubscriptionItemOrThrow(workspace.id);
const productPrice = await this.stripePriceService.getStripePrice(
AvailableProduct.BasePlan,
newInterval,
);
let productPrice;
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) {
throw new Error(

View File

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

View File

@ -23,7 +23,10 @@ export class StripeProductService {
}
async getAllProducts() {
const products = await this.stripe.products.list();
const products = await this.stripe.products.list({
active: true,
limit: 100,
});
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;
priceUsageBased: BillingUsageType;
isBaseProduct: 'true' | 'false';
[key: string]: string;
}
| 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 = {
planKey: BillingPlanKey.PRO,
priceUsageBased: BillingUsageType.METERED,
isBaseProduct: 'true',
};
expect(isStripeValidProductMetadata(metadata)).toBe(true);
@ -22,6 +23,7 @@ describe('isStripeValidProductMetadata', () => {
const metadata: Stripe.Metadata = {
planKey: BillingPlanKey.ENTERPRISE,
priceUsageBased: BillingUsageType.METERED,
isBaseProduct: 'false',
randomKey: 'randomValue',
};
@ -32,6 +34,7 @@ describe('isStripeValidProductMetadata', () => {
const metadata: Stripe.Metadata = {
planKey: 'invalid',
priceUsageBased: BillingUsageType.METERED,
isBaseProduct: 'invalid',
};
expect(isStripeValidProductMetadata(metadata)).toBe(false);
@ -41,6 +44,7 @@ describe('isStripeValidProductMetadata', () => {
const metadata: Stripe.Metadata = {
planKey: BillingPlanKey.PRO,
priceUsageBased: 'invalid',
isBaseProduct: 'true',
};
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 { 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', () => {
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({
stripeMeterId: 'met_123',
@ -74,7 +74,7 @@ describe('transformStripeMeterDataToMeterRepositoryData', () => {
},
};
const result = transformStripeMeterDataToMeterRepositoryData(data);
const result = transformStripeMeterToDatabaseMeter(data);
expect(result).toEqual({
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 { 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 { transformStripePriceDataToPriceRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-price-data-to-price-repository-data.util';
describe('transformStripePriceDataToPriceRepositoryData', () => {
import { transformStripePriceToDatabasePrice } from 'src/engine/core-modules/billing/utils/transform-stripe-price-to-database-price.util';
describe('transformStripePriceToDatabasePrice', () => {
const createMockPrice = (overrides = {}): Stripe.Price =>
({
id: 'price_123',
@ -34,7 +34,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
it('should transform basic price data correctly', () => {
const mockPrice = createMockPrice();
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
const result = transformStripePriceToDatabasePrice(mockPrice);
expect(result).toEqual({
stripePriceId: 'price_123',
@ -73,7 +73,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
const mockPrice = createMockPrice({
tax_behavior: stripeTaxBehavior as Stripe.Price.TaxBehavior,
});
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
const result = transformStripePriceToDatabasePrice(mockPrice);
expect(result.taxBehavior).toBe(expected);
},
@ -88,7 +88,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
const mockPrice = createMockPrice({
type: stripeType as Stripe.Price.Type,
});
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
const result = transformStripePriceToDatabasePrice(mockPrice);
expect(result.type).toBe(expected);
});
@ -104,7 +104,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
const mockPrice = createMockPrice({
billing_scheme: stripeScheme as Stripe.Price.BillingScheme,
});
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
const result = transformStripePriceToDatabasePrice(mockPrice);
expect(result.billingScheme).toBe(expected);
},
@ -120,7 +120,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
meter: 'meter_123',
},
});
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
const result = transformStripePriceToDatabasePrice(mockPrice);
expect(result.stripeMeterId).toBe('meter_123');
expect(result.usageType).toBe(BillingUsageType.METERED);
@ -139,7 +139,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
meter: null,
},
});
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
const result = transformStripePriceToDatabasePrice(mockPrice);
expect(result.interval).toBe(expected);
});
@ -162,7 +162,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
tiers: mockTiers,
tiers_mode: stripeTiersMode as Stripe.Price.TiersMode,
});
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
const result = transformStripePriceToDatabasePrice(mockPrice);
expect(result.tiersMode).toBe(expected);
expect(result.tiers).toEqual(mockTiers);
@ -179,7 +179,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
const mockPrice = createMockPrice({
transform_quantity: transformQuantity,
});
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
const result = transformStripePriceToDatabasePrice(mockPrice);
expect(result.transformQuantity).toEqual(transformQuantity);
});
@ -192,7 +192,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
},
};
const mockPrice = createMockPrice({ currency_options: currencyOptions });
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
const result = transformStripePriceToDatabasePrice(mockPrice);
expect(result.currencyOptions).toEqual(currencyOptions);
});
@ -206,7 +206,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
tiers: null,
currency_options: null,
});
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
const result = transformStripePriceToDatabasePrice(mockPrice);
expect(result.nickname).toBeUndefined();
expect(result.unitAmount).toBeUndefined();

View File

@ -1,7 +1,7 @@
import Stripe from 'stripe';
import { transformStripeProductDataToProductRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-product-data-to-product-repository-data.util';
describe('transformStripeProductDataToProductRepositoryData', () => {
import { transformStripeProductToDatabaseProduct } from 'src/engine/core-modules/billing/utils/transform-stripe-product-to-database-product.util';
describe('transformStripeProductToDatabaseProduct', () => {
it('should return the correct data', () => {
const data: Stripe.Product = {
id: 'prod_123',
@ -28,7 +28,7 @@ describe('transformStripeProductDataToProductRepositoryData', () => {
metadata: { key: 'value' },
};
const result = transformStripeProductDataToProductRepositoryData(data);
const result = transformStripeProductToDatabaseProduct(data);
expect(result).toEqual({
stripeProductId: 'prod_123',
@ -67,7 +67,7 @@ describe('transformStripeProductDataToProductRepositoryData', () => {
metadata: {},
};
const result = transformStripeProductDataToProductRepositoryData(data);
const result = transformStripeProductToDatabaseProduct(data);
expect(result).toEqual({
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 hasPriceUsageBased = isValidPriceUsageBased(metadata.priceUsageBased);
const hasIsBaseProduct =
metadata.isBaseProduct === 'true' || metadata.isBaseProduct === 'false';
return hasBillingPlanKey && hasPriceUsageBased;
return hasBillingPlanKey && hasPriceUsageBased && hasIsBaseProduct;
}
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 { BillingMeterStatus } from 'src/engine/core-modules/billing/enums/billing-meter-status.enum';
export const transformStripeMeterDataToMeterRepositoryData = (
export const transformStripeMeterToDatabaseMeter = (
data: Stripe.Billing.Meter,
) => {
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 { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
export const transformStripePriceDataToPriceRepositoryData = (
data: Stripe.Price,
) => {
export const transformStripePriceToDatabasePrice = (data: Stripe.Price) => {
return {
stripePriceId: data.id,
active: data.active,

View File

@ -1,13 +1,13 @@
import Stripe from 'stripe';
export const transformStripeProductDataToProductRepositoryData = (
export const transformStripeProductToDatabaseProduct = (
data: Stripe.Product,
) => {
return {
stripeProductId: data.id,
name: data.name,
active: data.active,
description: data.description,
description: data.description ?? '',
images: data.images,
marketingFeatures: data.marketing_features,
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';
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 { 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()
export class BillingWebhookEntitlementService {
protected readonly logger = new Logger(BillingWebhookEntitlementService.name);
@ -39,7 +39,7 @@ export class BillingWebhookEntitlementService {
const workspaceId = billingSubscription.workspaceId;
await this.billingEntitlementRepository.upsert(
transformStripeEntitlementUpdatedEventToEntitlementRepositoryData(
transformStripeEntitlementUpdatedEventToDatabaseEntitlement(
workspaceId,
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 { 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 { transformStripeMeterDataToMeterRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-meter-data-to-meter-repository-data.util';
import { transformStripePriceEventToPriceRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-price-event-to-price-repository-data.util';
import { transformStripeMeterToDatabaseMeter } from 'src/engine/core-modules/billing/utils/transform-stripe-meter-to-database-meter.util';
import { transformStripePriceEventToDatabasePrice } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-price-event-to-database-price.util';
@Injectable()
export class BillingWebhookPriceService {
protected readonly logger = new Logger(BillingWebhookPriceService.name);
@ -48,7 +49,7 @@ export class BillingWebhookPriceService {
const meterData = await this.stripeBillingMeterService.getMeter(meterId);
await this.billingMeterRepository.upsert(
transformStripeMeterDataToMeterRepositoryData(meterData),
transformStripeMeterToDatabaseMeter(meterData),
{
conflictPaths: ['stripeMeterId'],
skipUpdateIfNoValuesChanged: true,
@ -57,7 +58,7 @@ export class BillingWebhookPriceService {
}
await this.billingPriceRepository.upsert(
transformStripePriceEventToPriceRepositoryData(data),
transformStripePriceEventToDatabasePrice(data),
{
conflictPaths: ['stripePriceId'],
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 { 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 { 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()
export class BillingWebhookProductService {
protected readonly logger = new Logger(BillingWebhookProductService.name);
@ -24,10 +24,10 @@ export class BillingWebhookProductService {
const metadata = data.object.metadata;
const productRepositoryData = isStripeValidProductMetadata(metadata)
? {
...transformStripeProductEventToProductRepositoryData(data),
...transformStripeProductEventToDatabaseProduct(data),
metadata,
}
: transformStripeProductEventToProductRepositoryData(data);
: transformStripeProductEventToDatabaseProduct(data);
await this.billingProductRepository.upsert(productRepositoryData, {
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 { 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 { transformStripeSubscriptionEventToCustomerRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-customer-repository-data.util';
import { transformStripeSubscriptionEventToSubscriptionItemRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-item-repository-data.util';
import { transformStripeSubscriptionEventToSubscriptionRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-repository-data.util';
import { transformStripeSubscriptionEventToDatabaseCustomer } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-customer.util';
import { transformStripeSubscriptionEventToDatabaseSubscriptionItem } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-subscription-item.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';
@Injectable()
export class BillingWebhookSubscriptionService {
@ -47,10 +47,7 @@ export class BillingWebhookSubscriptionService {
}
await this.billingCustomerRepository.upsert(
transformStripeSubscriptionEventToCustomerRepositoryData(
workspaceId,
data,
),
transformStripeSubscriptionEventToDatabaseCustomer(workspaceId, data),
{
conflictPaths: ['workspaceId'],
skipUpdateIfNoValuesChanged: true,
@ -58,10 +55,7 @@ export class BillingWebhookSubscriptionService {
);
await this.billingSubscriptionRepository.upsert(
transformStripeSubscriptionEventToSubscriptionRepositoryData(
workspaceId,
data,
),
transformStripeSubscriptionEventToDatabaseSubscription(workspaceId, data),
{
conflictPaths: ['stripeSubscriptionId'],
skipUpdateIfNoValuesChanged: true,
@ -74,7 +68,7 @@ export class BillingWebhookSubscriptionService {
});
await this.billingSubscriptionItemRepository.upsert(
transformStripeSubscriptionEventToSubscriptionItemRepositoryData(
transformStripeSubscriptionEventToDatabaseSubscriptionItem(
billingSubscription.id,
data,
),

View File

@ -1,9 +1,9 @@
import Stripe from 'stripe';
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', () => {
const data: Stripe.EntitlementsActiveEntitlementSummaryUpdatedEvent.Data = {
object: {
@ -27,11 +27,10 @@ describe('transformStripeEntitlementUpdatedEventToEntitlementRepositoryData', ()
},
};
const result =
transformStripeEntitlementUpdatedEventToEntitlementRepositoryData(
'workspaceId',
data,
);
const result = transformStripeEntitlementUpdatedEventToDatabaseEntitlement(
'workspaceId',
data,
);
expect(result).toEqual([
{
@ -66,11 +65,10 @@ describe('transformStripeEntitlementUpdatedEventToEntitlementRepositoryData', ()
},
};
const result =
transformStripeEntitlementUpdatedEventToEntitlementRepositoryData(
'workspaceId',
data,
);
const result = transformStripeEntitlementUpdatedEventToDatabaseEntitlement(
'workspaceId',
data,
);
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 { 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 { 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 = {}) => ({
object: {
id: 'price_123',
@ -34,9 +34,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
it('should transform basic price data correctly', () => {
const mockData = createMockPriceData();
const result = transformStripePriceEventToPriceRepositoryData(
mockData as any,
);
const result = transformStripePriceEventToDatabasePrice(mockData as any);
expect(result).toEqual({
stripePriceId: 'price_123',
@ -74,9 +72,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
const mockData = createMockPriceData({
tax_behavior: stripeTaxBehavior,
});
const result = transformStripePriceEventToPriceRepositoryData(
mockData as any,
);
const result = transformStripePriceEventToDatabasePrice(mockData as any);
expect(result.taxBehavior).toBe(expectedTaxBehavior);
});
@ -90,9 +86,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
priceTypes.forEach(([stripeType, expectedType]) => {
const mockData = createMockPriceData({ type: stripeType });
const result = transformStripePriceEventToPriceRepositoryData(
mockData as any,
);
const result = transformStripePriceEventToDatabasePrice(mockData as any);
expect(result.type).toBe(expectedType);
});
@ -106,9 +100,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
billingSchemes.forEach(([stripeScheme, expectedScheme]) => {
const mockData = createMockPriceData({ billing_scheme: stripeScheme });
const result = transformStripePriceEventToPriceRepositoryData(
mockData as any,
);
const result = transformStripePriceEventToDatabasePrice(mockData as any);
expect(result.billingScheme).toBe(expectedScheme);
});
@ -124,9 +116,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
const mockData = createMockPriceData({
recurring: { usage_type: stripeUsageType, interval: 'month' },
});
const result = transformStripePriceEventToPriceRepositoryData(
mockData as any,
);
const result = transformStripePriceEventToDatabasePrice(mockData as any);
expect(result.usageType).toBe(expectedUsageType);
});
@ -140,9 +130,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
tiersModes.forEach(([stripeTiersMode, expectedTiersMode]) => {
const mockData = createMockPriceData({ tiers_mode: stripeTiersMode });
const result = transformStripePriceEventToPriceRepositoryData(
mockData as any,
);
const result = transformStripePriceEventToDatabasePrice(mockData as any);
expect(result.tiersMode).toBe(expectedTiersMode);
});
@ -160,9 +148,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
const mockData = createMockPriceData({
recurring: { usage_type: 'licensed', interval: stripeInterval },
});
const result = transformStripePriceEventToPriceRepositoryData(
mockData as any,
);
const result = transformStripePriceEventToDatabasePrice(mockData as any);
expect(result.interval).toBe(expectedInterval);
});
@ -180,9 +166,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
tiers_mode: 'graduated',
});
const result = transformStripePriceEventToPriceRepositoryData(
mockData as any,
);
const result = transformStripePriceEventToDatabasePrice(mockData as any);
expect(result.billingScheme).toBe(BillingPriceBillingScheme.TIERED);
expect(result.tiers).toEqual(mockTiers);
@ -204,9 +188,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
transform_quantity: mockTransformQuantity,
});
const result = transformStripePriceEventToPriceRepositoryData(
mockData as any,
);
const result = transformStripePriceEventToDatabasePrice(mockData as any);
expect(result.stripeMeterId).toBe('meter_123');
expect(result.usageType).toBe(BillingUsageType.METERED);
@ -225,9 +207,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
currency_options: mockCurrencyOptions,
});
const result = transformStripePriceEventToPriceRepositoryData(
mockData as any,
);
const result = transformStripePriceEventToDatabasePrice(mockData as any);
expect(result.currencyOptions).toEqual(mockCurrencyOptions);
});

View File

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

View File

@ -1,8 +1,8 @@
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 { 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 mockTimestamp = 1672531200; // 2023-01-01 00:00:00 UTC
@ -39,7 +39,7 @@ describe('transformStripeSubscriptionEventToSubscriptionRepositoryData', () => {
it('should transform basic subscription data correctly', () => {
const mockData = createMockSubscriptionData();
const result = transformStripeSubscriptionEventToSubscriptionRepositoryData(
const result = transformStripeSubscriptionEventToDatabaseSubscription(
mockWorkspaceId,
mockData as any,
);
@ -83,11 +83,10 @@ describe('transformStripeSubscriptionEventToSubscriptionRepositoryData', () => {
const mockData = createMockSubscriptionData({
status: stripeStatus,
});
const result =
transformStripeSubscriptionEventToSubscriptionRepositoryData(
mockWorkspaceId,
mockData as any,
);
const result = transformStripeSubscriptionEventToDatabaseSubscription(
mockWorkspaceId,
mockData as any,
);
expect(result.status).toBe(expectedStatus);
});
@ -102,7 +101,7 @@ describe('transformStripeSubscriptionEventToSubscriptionRepositoryData', () => {
trial_end: trialEnd,
});
const result = transformStripeSubscriptionEventToSubscriptionRepositoryData(
const result = transformStripeSubscriptionEventToDatabaseSubscription(
mockWorkspaceId,
mockData as any,
);
@ -125,7 +124,7 @@ describe('transformStripeSubscriptionEventToSubscriptionRepositoryData', () => {
},
});
const result = transformStripeSubscriptionEventToSubscriptionRepositoryData(
const result = transformStripeSubscriptionEventToDatabaseSubscription(
mockWorkspaceId,
mockData as any,
);
@ -148,7 +147,7 @@ describe('transformStripeSubscriptionEventToSubscriptionRepositoryData', () => {
},
});
const result = transformStripeSubscriptionEventToSubscriptionRepositoryData(
const result = transformStripeSubscriptionEventToDatabaseSubscription(
mockWorkspaceId,
mockData as any,
);
@ -172,11 +171,10 @@ describe('transformStripeSubscriptionEventToSubscriptionRepositoryData', () => {
const mockData = createMockSubscriptionData({
collection_method: stripeMethod,
});
const result =
transformStripeSubscriptionEventToSubscriptionRepositoryData(
mockWorkspaceId,
mockData as any,
);
const result = transformStripeSubscriptionEventToDatabaseSubscription(
mockWorkspaceId,
mockData as any,
);
expect(result.collectionMethod).toBe(expectedMethod);
});
@ -187,7 +185,7 @@ describe('transformStripeSubscriptionEventToSubscriptionRepositoryData', () => {
currency: 'eur',
});
const result = transformStripeSubscriptionEventToSubscriptionRepositoryData(
const result = transformStripeSubscriptionEventToDatabaseSubscription(
mockWorkspaceId,
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 { 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,
) => {
return {

View File

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

View File

@ -1,6 +1,6 @@
import Stripe from 'stripe';
export const transformStripeSubscriptionEventToCustomerRepositoryData = (
export const transformStripeSubscriptionEventToDatabaseCustomer = (
workspaceId: string,
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 { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
export const transformStripeSubscriptionEventToSubscriptionRepositoryData = (
export const transformStripeSubscriptionEventToDatabaseSubscription = (
workspaceId: string,
data:
| Stripe.CustomerSubscriptionUpdatedEvent.Data

View File

@ -1,6 +1,6 @@
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 { 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';
@ -17,8 +17,8 @@ class Billing {
@Field(() => String, { nullable: true })
billingUrl?: string;
@Field(() => [TrialPeriodDTO])
trialPeriods: TrialPeriodDTO[];
@Field(() => [BillingTrialPeriodDTO])
trialPeriods: BillingTrialPeriodDTO[];
}
@ObjectType()

View File

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