add fetch billing products from tables instead of env variables (#9601)
Solves https://github.com/twentyhq/private-issues/issues/237 **TLDR:** - Fetches billing products and prices from the tables BilllingProducts and BillingPrices instead of fetching the product from the environment variables and the prices from the stripe API. - Adds new feature flag for this feature - Fixes calls used to fetch stripe products and prices for the command Billing Sync Plans Data. **In order to test:** 1. Have the environment variable IS_BILLING_ENABLED set to true and add the other required environment variables for Billing to work 2. Do a database reset (to ensure that the new feature flag is properly added and that the billing tables are created) 3. Run the command: `npx nx run twenty-server:command billing:sync-plans-data` (if you don't do that the products and prices will not be present in the database) 4. Run the server , the frontend, the worker, and the stripe listen command (`stripe listen --forward-to http://localhost:3000/billing/webhooks`) 5. Buy a subscription for the Acme workspace and play with the project **Doing** I think there is some room of progress for the function formatProductPrices, I used a similar version that was done before, I'll look into that.
This commit is contained in:
committed by
GitHub
parent
3d2bb03c6d
commit
7d30b7577d
@ -50,6 +50,11 @@ export const seedFeatureFlags = async (
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IsBillingPlansEnabled,
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IsGmailSendEmailScopeEnabled,
|
||||
workspaceId: workspaceId,
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddNonNullableProductDescription1737127856478
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddNonNullableProductDescription1737127856478';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingProduct" ALTER COLUMN "description" SET DEFAULT ''`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingProduct" ALTER COLUMN "description" SET NOT NULL`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingProduct" ALTER COLUMN "description" DROP DEFAULT`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."billingProduct" ALTER COLUMN "description" DROP NOT NULL`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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',
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1,16 +1,24 @@
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { BillingSessionInput } from 'src/engine/core-modules/billing/dto/billing-session.input';
|
||||
import { CheckoutSessionInput } from 'src/engine/core-modules/billing/dto/checkout-session.input';
|
||||
import { ProductPricesEntity } from 'src/engine/core-modules/billing/dto/product-prices.entity';
|
||||
import { ProductInput } from 'src/engine/core-modules/billing/dto/product.input';
|
||||
import { SessionEntity } from 'src/engine/core-modules/billing/dto/session.entity';
|
||||
import { UpdateBillingEntity } from 'src/engine/core-modules/billing/dto/update-billing.entity';
|
||||
import { GraphQLError } from 'graphql';
|
||||
|
||||
import { BillingCheckoutSessionInput } from 'src/engine/core-modules/billing/dtos/inputs/billing-checkout-session.input';
|
||||
import { BillingProductInput } from 'src/engine/core-modules/billing/dtos/inputs/billing-product.input';
|
||||
import { BillingSessionInput } from 'src/engine/core-modules/billing/dtos/inputs/billing-session.input';
|
||||
import { BillingPlanOutput } from 'src/engine/core-modules/billing/dtos/outputs/billing-plan.output';
|
||||
import { BillingProductPricesOutput } from 'src/engine/core-modules/billing/dtos/outputs/billing-product-prices.output';
|
||||
import { BillingSessionOutput } from 'src/engine/core-modules/billing/dtos/outputs/billing-session.output';
|
||||
import { BillingUpdateOutput } from 'src/engine/core-modules/billing/dtos/outputs/billing-update.output';
|
||||
import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum';
|
||||
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
|
||||
import { BillingPlanService } from 'src/engine/core-modules/billing/services/billing-plan.service';
|
||||
import { BillingPortalWorkspaceService } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service';
|
||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||
import { StripePriceService } from 'src/engine/core-modules/billing/stripe/services/stripe-price.service';
|
||||
import { formatBillingDatabaseProductToGraphqlDTO } from 'src/engine/core-modules/billing/utils/format-database-product-to-graphql-dto.util';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||
@ -24,20 +32,26 @@ export class BillingResolver {
|
||||
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||
private readonly billingPortalWorkspaceService: BillingPortalWorkspaceService,
|
||||
private readonly stripePriceService: StripePriceService,
|
||||
private readonly billingPlanService: BillingPlanService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
) {}
|
||||
|
||||
@Query(() => ProductPricesEntity)
|
||||
async getProductPrices(@Args() { product }: ProductInput) {
|
||||
@Query(() => BillingProductPricesOutput)
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
async getProductPrices(
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@Args() { product }: BillingProductInput,
|
||||
) {
|
||||
const productPrices =
|
||||
await this.stripePriceService.getStripePrices(product);
|
||||
|
||||
return {
|
||||
totalNumberOfPrices: productPrices.length,
|
||||
productPrices: productPrices,
|
||||
productPrices,
|
||||
};
|
||||
}
|
||||
|
||||
@Query(() => SessionEntity)
|
||||
@Query(() => BillingSessionOutput)
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
async billingPortalSession(
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@ -51,7 +65,7 @@ export class BillingResolver {
|
||||
};
|
||||
}
|
||||
|
||||
@Mutation(() => SessionEntity)
|
||||
@Mutation(() => BillingSessionOutput)
|
||||
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
|
||||
async checkoutSession(
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
@ -62,15 +76,37 @@ export class BillingResolver {
|
||||
successUrlPath,
|
||||
plan,
|
||||
requirePaymentMethod,
|
||||
}: CheckoutSessionInput,
|
||||
}: BillingCheckoutSessionInput,
|
||||
) {
|
||||
const productPrice = await this.stripePriceService.getStripePrice(
|
||||
AvailableProduct.BasePlan,
|
||||
recurringInterval,
|
||||
);
|
||||
const isBillingPlansEnabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IsBillingPlansEnabled,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
let productPrice;
|
||||
|
||||
if (isBillingPlansEnabled) {
|
||||
const baseProduct = await this.billingPlanService.getPlanBaseProduct(
|
||||
plan ?? BillingPlanKey.PRO,
|
||||
);
|
||||
|
||||
if (!baseProduct) {
|
||||
throw new GraphQLError('Base product not found');
|
||||
}
|
||||
|
||||
productPrice = baseProduct.billingPrices.find(
|
||||
(price) => price.interval === recurringInterval,
|
||||
);
|
||||
} else {
|
||||
productPrice = await this.stripePriceService.getStripePrice(
|
||||
AvailableProduct.BasePlan,
|
||||
recurringInterval,
|
||||
);
|
||||
}
|
||||
|
||||
if (!productPrice) {
|
||||
throw new Error(
|
||||
throw new GraphQLError(
|
||||
'Product price not found for the given recurring interval',
|
||||
);
|
||||
}
|
||||
@ -87,11 +123,19 @@ export class BillingResolver {
|
||||
};
|
||||
}
|
||||
|
||||
@Mutation(() => UpdateBillingEntity)
|
||||
@Mutation(() => BillingUpdateOutput)
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
async updateBillingSubscription(@AuthWorkspace() workspace: Workspace) {
|
||||
await this.billingSubscriptionService.applyBillingSubscription(workspace);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@Query(() => [BillingPlanOutput])
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
async plans(): Promise<BillingPlanOutput[]> {
|
||||
const plans = await this.billingPlanService.getPlans();
|
||||
|
||||
return plans.map(formatBillingDatabaseProductToGraphqlDTO);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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`);
|
||||
|
||||
@ -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[];
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
},
|
||||
});
|
||||
@ -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;
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import { BillingPriceLicensedDTO } from 'src/engine/core-modules/billing/dtos/billing-price-licensed.dto';
|
||||
import { BillingPriceMeteredDTO } from 'src/engine/core-modules/billing/dtos/billing-price-metered.dto';
|
||||
import { BillingPriceUnionDTO } from 'src/engine/core-modules/billing/dtos/billing-price-union.dto';
|
||||
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
||||
|
||||
@ObjectType()
|
||||
export class BillingProductDTO {
|
||||
@Field(() => String)
|
||||
name: string;
|
||||
|
||||
@Field(() => String)
|
||||
description: string;
|
||||
|
||||
@Field(() => [String], { nullable: true })
|
||||
images: string[];
|
||||
|
||||
@Field(() => BillingUsageType)
|
||||
type: BillingUsageType;
|
||||
|
||||
@Field(() => [BillingPriceUnionDTO], { nullable: 'items' })
|
||||
prices: Array<BillingPriceLicensedDTO | BillingPriceMeteredDTO>;
|
||||
}
|
||||
@ -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;
|
||||
@ -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()
|
||||
@ -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()
|
||||
@ -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[];
|
||||
}
|
||||
@ -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[];
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType()
|
||||
export class SessionEntity {
|
||||
export class BillingSessionOutput {
|
||||
@Field(() => String, { nullable: true })
|
||||
url: string;
|
||||
}
|
||||
@ -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',
|
||||
})
|
||||
@ -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;
|
||||
|
||||
@ -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',
|
||||
});
|
||||
|
||||
@ -0,0 +1,107 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
BillingException,
|
||||
BillingExceptionCode,
|
||||
} from 'src/engine/core-modules/billing/billing.exception';
|
||||
import { BillingProduct } from 'src/engine/core-modules/billing/entities/billing-product.entity';
|
||||
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
|
||||
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
||||
import { BillingGetPlanResult } from 'src/engine/core-modules/billing/types/billing-get-plan-result.type';
|
||||
|
||||
@Injectable()
|
||||
export class BillingPlanService {
|
||||
protected readonly logger = new Logger(BillingPlanService.name);
|
||||
constructor(
|
||||
@InjectRepository(BillingProduct, 'core')
|
||||
private readonly billingProductRepository: Repository<BillingProduct>,
|
||||
) {}
|
||||
|
||||
async getProductsByProductMetadata({
|
||||
planKey,
|
||||
priceUsageBased,
|
||||
isBaseProduct,
|
||||
}: {
|
||||
planKey: BillingPlanKey;
|
||||
priceUsageBased: BillingUsageType;
|
||||
isBaseProduct: 'true' | 'false';
|
||||
}): Promise<BillingProduct[]> {
|
||||
const products = await this.billingProductRepository.find({
|
||||
where: {
|
||||
metadata: {
|
||||
planKey,
|
||||
priceUsageBased,
|
||||
isBaseProduct,
|
||||
},
|
||||
active: true,
|
||||
},
|
||||
relations: ['billingPrices'],
|
||||
});
|
||||
|
||||
return products;
|
||||
}
|
||||
|
||||
async getPlanBaseProduct(planKey: BillingPlanKey): Promise<BillingProduct> {
|
||||
const [baseProduct] = await this.getProductsByProductMetadata({
|
||||
planKey,
|
||||
priceUsageBased: BillingUsageType.LICENSED,
|
||||
isBaseProduct: 'true',
|
||||
});
|
||||
|
||||
return baseProduct;
|
||||
}
|
||||
|
||||
async getPlans(): Promise<BillingGetPlanResult[]> {
|
||||
const planKeys = Object.values(BillingPlanKey);
|
||||
|
||||
const products = await this.billingProductRepository.find({
|
||||
where: {
|
||||
active: true,
|
||||
},
|
||||
relations: ['billingPrices'],
|
||||
});
|
||||
|
||||
return planKeys.map((planKey) => {
|
||||
const planProducts = products
|
||||
.filter((product) => product.metadata.planKey === planKey)
|
||||
.map((product) => {
|
||||
return {
|
||||
...product,
|
||||
billingPrices: product.billingPrices.filter(
|
||||
(price) => price.active,
|
||||
),
|
||||
};
|
||||
});
|
||||
const baseProduct = planProducts.find(
|
||||
(product) => product.metadata.isBaseProduct === 'true',
|
||||
);
|
||||
|
||||
if (!baseProduct) {
|
||||
throw new BillingException(
|
||||
'Base product not found',
|
||||
BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const meteredProducts = planProducts.filter(
|
||||
(product) =>
|
||||
product.metadata.priceUsageBased === BillingUsageType.METERED,
|
||||
);
|
||||
const otherLicensedProducts = planProducts.filter(
|
||||
(product) =>
|
||||
product.metadata.priceUsageBased === BillingUsageType.LICENSED &&
|
||||
product.metadata.isBaseProduct === 'false',
|
||||
);
|
||||
|
||||
return {
|
||||
planKey,
|
||||
baseProduct,
|
||||
meteredProducts,
|
||||
otherLicensedProducts,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -6,18 +6,25 @@ import assert from 'assert';
|
||||
import Stripe from 'stripe';
|
||||
import { Not, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
BillingException,
|
||||
BillingExceptionCode,
|
||||
} from 'src/engine/core-modules/billing/billing.exception';
|
||||
import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
|
||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||
import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum';
|
||||
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
|
||||
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
|
||||
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
||||
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
|
||||
import { BillingPlanService } from 'src/engine/core-modules/billing/services/billing-plan.service';
|
||||
import { StripePriceService } from 'src/engine/core-modules/billing/stripe/services/stripe-price.service';
|
||||
import { StripeSubscriptionItemService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service';
|
||||
import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
@Injectable()
|
||||
export class BillingSubscriptionService {
|
||||
protected readonly logger = new Logger(BillingSubscriptionService.name);
|
||||
@ -25,7 +32,9 @@ export class BillingSubscriptionService {
|
||||
private readonly stripeSubscriptionService: StripeSubscriptionService,
|
||||
private readonly stripePriceService: StripePriceService,
|
||||
private readonly stripeSubscriptionItemService: StripeSubscriptionItemService,
|
||||
private readonly billingPlanService: BillingPlanService,
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
@InjectRepository(BillingEntitlement, 'core')
|
||||
private readonly billingEntitlementRepository: Repository<BillingEntitlement>,
|
||||
@InjectRepository(BillingSubscription, 'core')
|
||||
@ -56,19 +65,37 @@ export class BillingSubscriptionService {
|
||||
'BILLING_STRIPE_BASE_PLAN_PRODUCT_ID',
|
||||
),
|
||||
) {
|
||||
const isBillingPlansEnabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IsBillingPlansEnabled,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const billingSubscription = await this.getCurrentBillingSubscriptionOrThrow(
|
||||
{ workspaceId },
|
||||
);
|
||||
|
||||
const getStripeProductId = isBillingPlansEnabled
|
||||
? (await this.billingPlanService.getPlanBaseProduct(BillingPlanKey.PRO))
|
||||
?.stripeProductId
|
||||
: stripeProductId;
|
||||
|
||||
if (!getStripeProductId) {
|
||||
throw new BillingException(
|
||||
'Base product not found',
|
||||
BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const billingSubscriptionItem =
|
||||
billingSubscription.billingSubscriptionItems.filter(
|
||||
(billingSubscriptionItem) =>
|
||||
billingSubscriptionItem.stripeProductId === stripeProductId,
|
||||
billingSubscriptionItem.stripeProductId === getStripeProductId,
|
||||
)?.[0];
|
||||
|
||||
if (!billingSubscriptionItem) {
|
||||
throw new Error(
|
||||
`Cannot find billingSubscriptionItem for product ${stripeProductId} for workspace ${workspaceId}`,
|
||||
`Cannot find billingSubscriptionItem for product ${getStripeProductId} for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
@ -127,7 +154,11 @@ export class BillingSubscriptionService {
|
||||
const billingSubscription = await this.getCurrentBillingSubscriptionOrThrow(
|
||||
{ workspaceId: workspace.id },
|
||||
);
|
||||
|
||||
const isBillingPlansEnabled =
|
||||
await this.featureFlagService.isFeatureEnabled(
|
||||
FeatureFlagKey.IsBillingPlansEnabled,
|
||||
workspace.id,
|
||||
);
|
||||
const newInterval =
|
||||
billingSubscription?.interval === SubscriptionInterval.Year
|
||||
? SubscriptionInterval.Month
|
||||
@ -136,10 +167,29 @@ export class BillingSubscriptionService {
|
||||
const billingSubscriptionItem =
|
||||
await this.getCurrentBillingSubscriptionItemOrThrow(workspace.id);
|
||||
|
||||
const productPrice = await this.stripePriceService.getStripePrice(
|
||||
AvailableProduct.BasePlan,
|
||||
newInterval,
|
||||
);
|
||||
let productPrice;
|
||||
|
||||
if (isBillingPlansEnabled) {
|
||||
const baseProduct = await this.billingPlanService.getPlanBaseProduct(
|
||||
BillingPlanKey.PRO,
|
||||
);
|
||||
|
||||
if (!baseProduct) {
|
||||
throw new BillingException(
|
||||
'Base product not found',
|
||||
BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
productPrice = baseProduct.billingPrices.find(
|
||||
(price) => price.interval === newInterval,
|
||||
);
|
||||
} else {
|
||||
productPrice = await this.stripePriceService.getStripePrice(
|
||||
AvailableProduct.BasePlan,
|
||||
newInterval,
|
||||
);
|
||||
}
|
||||
|
||||
if (!productPrice) {
|
||||
throw new Error(
|
||||
|
||||
@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { ProductPriceEntity } from 'src/engine/core-modules/billing/dto/product-price.entity';
|
||||
import { BillingProductPriceDTO } from 'src/engine/core-modules/billing/dtos/billing-product-price.dto';
|
||||
import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum';
|
||||
import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
@ -46,10 +46,10 @@ export class StripePriceService {
|
||||
if (product === AvailableProduct.BasePlan) {
|
||||
return this.environmentService.get('BILLING_STRIPE_BASE_PLAN_PRODUCT_ID');
|
||||
}
|
||||
} // PD:,will be eliminated after refactoring
|
||||
}
|
||||
|
||||
formatProductPrices(prices: Stripe.Price[]): ProductPriceEntity[] {
|
||||
const productPrices: ProductPriceEntity[] = Object.values(
|
||||
formatProductPrices(prices: Stripe.Price[]): BillingProductPriceDTO[] {
|
||||
const productPrices: BillingProductPriceDTO[] = Object.values(
|
||||
prices
|
||||
.filter((item) => item.recurring?.interval && item.unit_amount)
|
||||
.reduce((acc, item: Stripe.Price) => {
|
||||
@ -68,7 +68,7 @@ export class StripePriceService {
|
||||
};
|
||||
}
|
||||
|
||||
return acc satisfies Record<string, ProductPriceEntity>;
|
||||
return acc satisfies Record<string, BillingProductPriceDTO>;
|
||||
}, {}),
|
||||
);
|
||||
|
||||
@ -76,8 +76,10 @@ export class StripePriceService {
|
||||
}
|
||||
|
||||
async getPricesByProductId(productId: string) {
|
||||
const prices = await this.stripe.prices.search({
|
||||
query: `product:'${productId}'`,
|
||||
const prices = await this.stripe.prices.list({
|
||||
product: productId,
|
||||
type: 'recurring',
|
||||
expand: ['data.currency_options', 'data.tiers'],
|
||||
});
|
||||
|
||||
return prices.data;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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[];
|
||||
};
|
||||
@ -5,6 +5,7 @@ export type BillingProductMetadata =
|
||||
| {
|
||||
planKey: BillingPlanKey;
|
||||
priceUsageBased: BillingUsageType;
|
||||
isBaseProduct: 'true' | 'false';
|
||||
[key: string]: string;
|
||||
}
|
||||
| Record<string, never>;
|
||||
|
||||
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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);
|
||||
|
||||
@ -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',
|
||||
@ -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();
|
||||
@ -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',
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
@ -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) => {
|
||||
|
||||
@ -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,
|
||||
};
|
||||
});
|
||||
};
|
||||
@ -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 {
|
||||
@ -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,
|
||||
@ -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
|
||||
@ -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,
|
||||
};
|
||||
});
|
||||
};
|
||||
@ -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,
|
||||
),
|
||||
@ -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,
|
||||
@ -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'],
|
||||
@ -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,
|
||||
),
|
||||
@ -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([
|
||||
{
|
||||
@ -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);
|
||||
});
|
||||
@ -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',
|
||||
@ -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,
|
||||
);
|
||||
@ -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,
|
||||
);
|
||||
@ -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,
|
||||
};
|
||||
});
|
||||
};
|
||||
@ -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 {
|
||||
@ -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
|
||||
@ -1,6 +1,6 @@
|
||||
import Stripe from 'stripe';
|
||||
|
||||
export const transformStripeSubscriptionEventToCustomerRepositoryData = (
|
||||
export const transformStripeSubscriptionEventToDatabaseCustomer = (
|
||||
workspaceId: string,
|
||||
data:
|
||||
| Stripe.CustomerSubscriptionUpdatedEvent.Data
|
||||
@ -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,
|
||||
};
|
||||
});
|
||||
};
|
||||
@ -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
|
||||
@ -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()
|
||||
|
||||
@ -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',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user