diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts index 03ae59dca..fd7da0e98 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts @@ -50,6 +50,11 @@ export const seedFeatureFlags = async ( workspaceId: workspaceId, value: true, }, + { + key: FeatureFlagKey.IsBillingPlansEnabled, + workspaceId: workspaceId, + value: true, + }, { key: FeatureFlagKey.IsGmailSendEmailScopeEnabled, workspaceId: workspaceId, diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/billing/1737127856478-addNonNullableProductDescription.ts b/packages/twenty-server/src/database/typeorm/core/migrations/billing/1737127856478-addNonNullableProductDescription.ts new file mode 100644 index 000000000..0888faf69 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/billing/1737127856478-addNonNullableProductDescription.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddNonNullableProductDescription1737127856478 + implements MigrationInterface +{ + name = 'AddNonNullableProductDescription1737127856478'; + + public async up(queryRunner: QueryRunner): Promise { + 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 { + 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`, + ); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts index 19c95edef..e35b8743b 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts @@ -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 { diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts index 093b0c481..8e5ae8fc1 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts @@ -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', } diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts index 3dddad725..ebab7647e 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts @@ -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, diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts index 34d55e277..11df9347c 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.resolver.ts @@ -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 { + const plans = await this.billingPlanService.getPlans(); + + return plans.map(formatBillingDatabaseProductToGraphqlDTO); + } } diff --git a/packages/twenty-server/src/engine/core-modules/billing/commands/billing-sync-plans-data.command.ts b/packages/twenty-server/src/engine/core-modules/billing/commands/billing-sync-plans-data.command.ts index a220c6a59..73f0a63cc 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/commands/billing-sync-plans-data.command.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/commands/billing-sync-plans-data.command.ts @@ -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`); diff --git a/packages/twenty-server/src/engine/core-modules/billing/dto/product-prices.entity.ts b/packages/twenty-server/src/engine/core-modules/billing/dto/product-prices.entity.ts deleted file mode 100644 index 02efc4174..000000000 --- a/packages/twenty-server/src/engine/core-modules/billing/dto/product-prices.entity.ts +++ /dev/null @@ -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[]; -} diff --git a/packages/twenty-server/src/engine/core-modules/billing/dtos/billing-price-licensed.dto.ts b/packages/twenty-server/src/engine/core-modules/billing/dtos/billing-price-licensed.dto.ts new file mode 100644 index 000000000..b6fc10d97 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/dtos/billing-price-licensed.dto.ts @@ -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; +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/dtos/billing-price-metered.dto.ts b/packages/twenty-server/src/engine/core-modules/billing/dtos/billing-price-metered.dto.ts new file mode 100644 index 000000000..84d7398c8 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/dtos/billing-price-metered.dto.ts @@ -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; +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/dtos/billing-price-tier.dto.ts b/packages/twenty-server/src/engine/core-modules/billing/dtos/billing-price-tier.dto.ts new file mode 100644 index 000000000..6765dd1c1 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/dtos/billing-price-tier.dto.ts @@ -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; +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/dtos/billing-price-union.dto.ts b/packages/twenty-server/src/engine/core-modules/billing/dtos/billing-price-union.dto.ts new file mode 100644 index 000000000..976781877 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/dtos/billing-price-union.dto.ts @@ -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; + }, +}); diff --git a/packages/twenty-server/src/engine/core-modules/billing/dto/product-price.entity.ts b/packages/twenty-server/src/engine/core-modules/billing/dtos/billing-product-price.dto.ts similarity index 92% rename from packages/twenty-server/src/engine/core-modules/billing/dto/product-price.entity.ts rename to packages/twenty-server/src/engine/core-modules/billing/dtos/billing-product-price.dto.ts index 011d880b2..6403d233d 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/dto/product-price.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/dtos/billing-product-price.dto.ts @@ -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; diff --git a/packages/twenty-server/src/engine/core-modules/billing/dtos/billing-product.dto.ts b/packages/twenty-server/src/engine/core-modules/billing/dtos/billing-product.dto.ts new file mode 100644 index 000000000..391a1ed27 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/dtos/billing-product.dto.ts @@ -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; +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/dto/trial-period.dto.ts b/packages/twenty-server/src/engine/core-modules/billing/dtos/billing-trial-period.dto.ts similarity index 85% rename from packages/twenty-server/src/engine/core-modules/billing/dto/trial-period.dto.ts rename to packages/twenty-server/src/engine/core-modules/billing/dtos/billing-trial-period.dto.ts index f8f9b8a5a..e78cadd19 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/dto/trial-period.dto.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/dtos/billing-trial-period.dto.ts @@ -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; diff --git a/packages/twenty-server/src/engine/core-modules/billing/dto/checkout-session.input.ts b/packages/twenty-server/src/engine/core-modules/billing/dtos/inputs/billing-checkout-session.input.ts similarity index 95% rename from packages/twenty-server/src/engine/core-modules/billing/dto/checkout-session.input.ts rename to packages/twenty-server/src/engine/core-modules/billing/dtos/inputs/billing-checkout-session.input.ts index b5c882be2..6efa6ca03 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/dto/checkout-session.input.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/dtos/inputs/billing-checkout-session.input.ts @@ -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() diff --git a/packages/twenty-server/src/engine/core-modules/billing/dto/product.input.ts b/packages/twenty-server/src/engine/core-modules/billing/dtos/inputs/billing-product.input.ts similarity index 89% rename from packages/twenty-server/src/engine/core-modules/billing/dto/product.input.ts rename to packages/twenty-server/src/engine/core-modules/billing/dtos/inputs/billing-product.input.ts index 126e1351d..e3eade45a 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/dto/product.input.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/dtos/inputs/billing-product.input.ts @@ -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() diff --git a/packages/twenty-server/src/engine/core-modules/billing/dto/billing-session.input.ts b/packages/twenty-server/src/engine/core-modules/billing/dtos/inputs/billing-session.input.ts similarity index 100% rename from packages/twenty-server/src/engine/core-modules/billing/dto/billing-session.input.ts rename to packages/twenty-server/src/engine/core-modules/billing/dtos/inputs/billing-session.input.ts diff --git a/packages/twenty-server/src/engine/core-modules/billing/dtos/outputs/billing-plan.output.ts b/packages/twenty-server/src/engine/core-modules/billing/dtos/outputs/billing-plan.output.ts new file mode 100644 index 000000000..765d38cbe --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/dtos/outputs/billing-plan.output.ts @@ -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[]; +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/dtos/outputs/billing-product-prices.output.ts b/packages/twenty-server/src/engine/core-modules/billing/dtos/outputs/billing-product-prices.output.ts new file mode 100644 index 000000000..c80bc23d9 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/dtos/outputs/billing-product-prices.output.ts @@ -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[]; +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/dto/session.entity.ts b/packages/twenty-server/src/engine/core-modules/billing/dtos/outputs/billing-session.output.ts similarity index 78% rename from packages/twenty-server/src/engine/core-modules/billing/dto/session.entity.ts rename to packages/twenty-server/src/engine/core-modules/billing/dtos/outputs/billing-session.output.ts index 745a7364b..a07ebf7a9 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/dto/session.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/dtos/outputs/billing-session.output.ts @@ -1,7 +1,7 @@ import { Field, ObjectType } from '@nestjs/graphql'; @ObjectType() -export class SessionEntity { +export class BillingSessionOutput { @Field(() => String, { nullable: true }) url: string; } diff --git a/packages/twenty-server/src/engine/core-modules/billing/dto/update-billing.entity.ts b/packages/twenty-server/src/engine/core-modules/billing/dtos/outputs/billing-update.output.ts similarity index 84% rename from packages/twenty-server/src/engine/core-modules/billing/dto/update-billing.entity.ts rename to packages/twenty-server/src/engine/core-modules/billing/dtos/outputs/billing-update.output.ts index ae8f8660d..fd57062c7 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/dto/update-billing.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/dtos/outputs/billing-update.output.ts @@ -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', }) diff --git a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-product.entity.ts b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-product.entity.ts index 7019d38ec..a226c8fda 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-product.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-product.entity.ts @@ -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; diff --git a/packages/twenty-server/src/engine/core-modules/billing/enums/billing-price-tiers-mode.enum.ts b/packages/twenty-server/src/engine/core-modules/billing/enums/billing-price-tiers-mode.enum.ts index 2d54ac874..f7372e1fa 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/enums/billing-price-tiers-mode.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/enums/billing-price-tiers-mode.enum.ts @@ -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', +}); diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-plan.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-plan.service.ts new file mode 100644 index 000000000..ba3d35678 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-plan.service.ts @@ -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, + ) {} + + async getProductsByProductMetadata({ + planKey, + priceUsageBased, + isBaseProduct, + }: { + planKey: BillingPlanKey; + priceUsageBased: BillingUsageType; + isBaseProduct: 'true' | 'false'; + }): Promise { + const products = await this.billingProductRepository.find({ + where: { + metadata: { + planKey, + priceUsageBased, + isBaseProduct, + }, + active: true, + }, + relations: ['billingPrices'], + }); + + return products; + } + + async getPlanBaseProduct(planKey: BillingPlanKey): Promise { + const [baseProduct] = await this.getProductsByProductMetadata({ + planKey, + priceUsageBased: BillingUsageType.LICENSED, + isBaseProduct: 'true', + }); + + return baseProduct; + } + + async getPlans(): Promise { + 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, + }; + }); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts index a43125e5b..7378f3f8b 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts @@ -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, @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( diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-price.service.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-price.service.ts index 83e4058f4..01a3c8d12 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-price.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-price.service.ts @@ -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; + return acc satisfies Record; }, {}), ); @@ -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; diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-product.service.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-product.service.ts index 51aa3f9c4..c9fd5526c 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-product.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-product.service.ts @@ -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; } diff --git a/packages/twenty-server/src/engine/core-modules/billing/types/billing-get-plan-result.type.ts b/packages/twenty-server/src/engine/core-modules/billing/types/billing-get-plan-result.type.ts new file mode 100644 index 000000000..3505e094d --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/types/billing-get-plan-result.type.ts @@ -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[]; +}; diff --git a/packages/twenty-server/src/engine/core-modules/billing/types/billing-product-metadata.type.ts b/packages/twenty-server/src/engine/core-modules/billing/types/billing-product-metadata.type.ts index 39245f0ae..382e22fa6 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/types/billing-product-metadata.type.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/types/billing-product-metadata.type.ts @@ -5,6 +5,7 @@ export type BillingProductMetadata = | { planKey: BillingPlanKey; priceUsageBased: BillingUsageType; + isBaseProduct: 'true' | 'false'; [key: string]: string; } | Record; diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/format-database-product-to-graphql-dto.util.spec.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/format-database-product-to-graphql-dto.util.spec.ts new file mode 100644 index 000000000..75a049a5f --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/format-database-product-to-graphql-dto.util.spec.ts @@ -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, + }, + ], + }, + ], + }); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/is-stripe-valid-product-metadata.util.spec.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/is-stripe-valid-product-metadata.util.spec.ts index 0cbc5f5d8..b2cf67415 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/is-stripe-valid-product-metadata.util.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/is-stripe-valid-product-metadata.util.spec.ts @@ -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); diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-meter-data-to-meter-repository-data.util.spec.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-meter-to-database-meter.util.spec.ts similarity index 89% rename from packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-meter-data-to-meter-repository-data.util.spec.ts rename to packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-meter-to-database-meter.util.spec.ts index 912bc557c..30d84cc91 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-meter-data-to-meter-repository-data.util.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-meter-to-database-meter.util.spec.ts @@ -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', diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-price-data-to-price-repository-data.util.spec.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-price-to-database-price.util.spec.ts similarity index 86% rename from packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-price-data-to-price-repository-data.util.spec.ts rename to packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-price-to-database-price.util.spec.ts index 81e8e24df..22893641e 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-price-data-to-price-repository-data.util.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-price-to-database-price.util.spec.ts @@ -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(); diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-product-data-to-product-repository-data.util.spec.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-product-to-database-product.util.spec.ts similarity index 83% rename from packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-product-data-to-product-repository-data.util.spec.ts rename to packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-product-to-database-product.util.spec.ts index 875a19de0..e14ebe833 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-product-data-to-product-repository-data.util.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-product-to-database-product.util.spec.ts @@ -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', diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/format-database-product-to-graphql-dto.util.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/format-database-product-to-graphql-dto.util.ts new file mode 100644 index 000000000..f5979f95c --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/utils/format-database-product-to-graphql-dto.util.ts @@ -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, + }; +}; diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/is-stripe-valid-product-metadata.util.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/is-stripe-valid-product-metadata.util.ts index cf2a8c955..a33b8e5b8 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/utils/is-stripe-valid-product-metadata.util.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/utils/is-stripe-valid-product-metadata.util.ts @@ -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) => { diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-entitlement-updated-event-to-entitlement-repository-data.util.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-entitlement-updated-event-to-entitlement-repository-data.util.ts deleted file mode 100644 index 7577f9a90..000000000 --- a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-entitlement-updated-event-to-entitlement-repository-data.util.ts +++ /dev/null @@ -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, - }; - }); - }; diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-meter-data-to-meter-repository-data.util.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-meter-to-database-meter.util.ts similarity index 94% rename from packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-meter-data-to-meter-repository-data.util.ts rename to packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-meter-to-database-meter.util.ts index e7ae84230..59370cbd3 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-meter-data-to-meter-repository-data.util.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-meter-to-database-meter.util.ts @@ -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 { diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-price-data-to-price-repository-data.util.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-price-to-database-price.util.ts similarity index 97% rename from packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-price-data-to-price-repository-data.util.ts rename to packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-price-to-database-price.util.ts index c5525b176..fdb0cf81c 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-price-data-to-price-repository-data.util.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-price-to-database-price.util.ts @@ -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, diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-product-data-to-product-repository-data.util.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-product-to-database-product.util.ts similarity index 84% rename from packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-product-data-to-product-repository-data.util.ts rename to packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-product-to-database-product.util.ts index f1e9413fc..5f9c11ac1 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-product-data-to-product-repository-data.util.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-product-to-database-product.util.ts @@ -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 diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-item-repository-data.util.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-item-repository-data.util.ts deleted file mode 100644 index 9c72971dc..000000000 --- a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-item-repository-data.util.ts +++ /dev/null @@ -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, - }; - }); - }; diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-entitlement.service.ts b/packages/twenty-server/src/engine/core-modules/billing/webhooks/services/billing-webhook-entitlement.service.ts similarity index 86% rename from packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-entitlement.service.ts rename to packages/twenty-server/src/engine/core-modules/billing/webhooks/services/billing-webhook-entitlement.service.ts index 2c3e28503..c73efac37 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-entitlement.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/webhooks/services/billing-webhook-entitlement.service.ts @@ -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, ), diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-price.service.ts b/packages/twenty-server/src/engine/core-modules/billing/webhooks/services/billing-webhook-price.service.ts similarity index 83% rename from packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-price.service.ts rename to packages/twenty-server/src/engine/core-modules/billing/webhooks/services/billing-webhook-price.service.ts index 660b1d95d..eca1edd71 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-price.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/webhooks/services/billing-webhook-price.service.ts @@ -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, diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-product.service.ts b/packages/twenty-server/src/engine/core-modules/billing/webhooks/services/billing-webhook-product.service.ts similarity index 87% rename from packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-product.service.ts rename to packages/twenty-server/src/engine/core-modules/billing/webhooks/services/billing-webhook-product.service.ts index 21d09abb4..0fd12dedb 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-product.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/webhooks/services/billing-webhook-product.service.ts @@ -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'], diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-subscription.service.ts b/packages/twenty-server/src/engine/core-modules/billing/webhooks/services/billing-webhook-subscription.service.ts similarity index 81% rename from packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-subscription.service.ts rename to packages/twenty-server/src/engine/core-modules/billing/webhooks/services/billing-webhook-subscription.service.ts index fba68f223..fcad8e097 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-subscription.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/webhooks/services/billing-webhook-subscription.service.ts @@ -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, ), diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-entitlement-updated-event-to-entitlement-repository-data.util.spec.ts b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/__tests__/transform-stripe-entitlement-updated-event-to-database-entitlement.util.spec.ts similarity index 77% rename from packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-entitlement-updated-event-to-entitlement-repository-data.util.spec.ts rename to packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/__tests__/transform-stripe-entitlement-updated-event-to-database-entitlement.util.spec.ts index 36b87bdd5..6077e8bb4 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-entitlement-updated-event-to-entitlement-repository-data.util.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/__tests__/transform-stripe-entitlement-updated-event-to-database-entitlement.util.spec.ts @@ -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([ { diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-price-event-to-price-repository-data.util.spec.ts b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/__tests__/transform-stripe-price-event-to-database-price.util.spec.ts similarity index 83% rename from packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-price-event-to-price-repository-data.util.spec.ts rename to packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/__tests__/transform-stripe-price-event-to-database-price.util.spec.ts index 5faa1385b..3ace9f346 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-price-event-to-price-repository-data.util.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/__tests__/transform-stripe-price-event-to-database-price.util.spec.ts @@ -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); }); diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-product-event-to-product-repository-data.util.spec.ts b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/__tests__/transform-stripe-product-event-to-database-product.util.spec.ts similarity index 84% rename from packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-product-event-to-product-repository-data.util.spec.ts rename to packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/__tests__/transform-stripe-product-event-to-database-product.util.spec.ts index 2a858a781..b8a496be1 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-product-event-to-product-repository-data.util.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/__tests__/transform-stripe-product-event-to-database-product.util.spec.ts @@ -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', diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-subscription-event-to-customer-repository-data.util.spec.ts b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/__tests__/transform-stripe-subscription-event-to-database-customer.util.spec.ts similarity index 80% rename from packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-subscription-event-to-customer-repository-data.util.spec.ts rename to packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/__tests__/transform-stripe-subscription-event-to-database-customer.util.spec.ts index be841607b..d8de729ef 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-subscription-event-to-customer-repository-data.util.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/__tests__/transform-stripe-subscription-event-to-database-customer.util.spec.ts @@ -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, ); diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-subscription-event-to-subscription-repository-data.util.spec.ts b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/__tests__/transform-stripe-subscription-event-to-database-subscription.util.spec.ts similarity index 84% rename from packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-subscription-event-to-subscription-repository-data.util.spec.ts rename to packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/__tests__/transform-stripe-subscription-event-to-database-subscription.util.spec.ts index 6c4a6cdb0..40823ffce 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/transform-stripe-subscription-event-to-subscription-repository-data.util.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/__tests__/transform-stripe-subscription-event-to-database-subscription.util.spec.ts @@ -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, ); diff --git a/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/transform-stripe-entitlement-updated-event-to-database-entitlement.util.ts b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/transform-stripe-entitlement-updated-event-to-database-entitlement.util.ts new file mode 100644 index 000000000..02279dca6 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/transform-stripe-entitlement-updated-event-to-database-entitlement.util.ts @@ -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, + }; + }); +}; diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-price-event-to-price-repository-data.util.ts b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/transform-stripe-price-event-to-database-price.util.ts similarity index 98% rename from packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-price-event-to-price-repository-data.util.ts rename to packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/transform-stripe-price-event-to-database-price.util.ts index 5ce42e1e5..66e0bb60d 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-price-event-to-price-repository-data.util.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/transform-stripe-price-event-to-database-price.util.ts @@ -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 { diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-product-event-to-product-repository-data.util.ts b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/transform-stripe-product-event-to-database-product.util.ts similarity index 85% rename from packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-product-event-to-product-repository-data.util.ts rename to packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/transform-stripe-product-event-to-database-product.util.ts index 2fea8d36c..de52681f4 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-product-event-to-product-repository-data.util.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/transform-stripe-product-event-to-database-product.util.ts @@ -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 diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-customer-repository-data.util.ts b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-customer.util.ts similarity index 80% rename from packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-customer-repository-data.util.ts rename to packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-customer.util.ts index 3cb313e27..5d14b1acc 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-customer-repository-data.util.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-customer.util.ts @@ -1,6 +1,6 @@ import Stripe from 'stripe'; -export const transformStripeSubscriptionEventToCustomerRepositoryData = ( +export const transformStripeSubscriptionEventToDatabaseCustomer = ( workspaceId: string, data: | Stripe.CustomerSubscriptionUpdatedEvent.Data diff --git a/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-subscription-item.util.ts b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-subscription-item.util.ts new file mode 100644 index 000000000..f6ecd5c65 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-subscription-item.util.ts @@ -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, + }; + }); +}; diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-repository-data.util.ts b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-subscription.util.ts similarity index 97% rename from packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-repository-data.util.ts rename to packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-subscription.util.ts index 53e37d9a1..8d58e3518 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-repository-data.util.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-subscription.util.ts @@ -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 diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts index dde6c9a69..2fdb7ed8c 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts @@ -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() diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts index 2cb2752ff..41cf38df9 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts @@ -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', }