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,
|
workspaceId: workspaceId,
|
||||||
value: true,
|
value: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: FeatureFlagKey.IsBillingPlansEnabled,
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: FeatureFlagKey.IsGmailSendEmailScopeEnabled,
|
key: FeatureFlagKey.IsGmailSendEmailScopeEnabled,
|
||||||
workspaceId: workspaceId,
|
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 { BillingWebhookEvent } from 'src/engine/core-modules/billing/enums/billing-webhook-events.enum';
|
||||||
import { BillingRestApiExceptionFilter } from 'src/engine/core-modules/billing/filters/billing-api-exception.filter';
|
import { BillingRestApiExceptionFilter } from 'src/engine/core-modules/billing/filters/billing-api-exception.filter';
|
||||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||||
import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing/services/billing-webhook-entitlement.service';
|
|
||||||
import { BillingWebhookPriceService } from 'src/engine/core-modules/billing/services/billing-webhook-price.service';
|
|
||||||
import { BillingWebhookProductService } from 'src/engine/core-modules/billing/services/billing-webhook-product.service';
|
|
||||||
import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing/services/billing-webhook-subscription.service';
|
|
||||||
import { StripeWebhookService } from 'src/engine/core-modules/billing/stripe/services/stripe-webhook.service';
|
import { StripeWebhookService } from 'src/engine/core-modules/billing/stripe/services/stripe-webhook.service';
|
||||||
|
import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-entitlement.service';
|
||||||
|
import { BillingWebhookPriceService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-price.service';
|
||||||
|
import { BillingWebhookProductService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-product.service';
|
||||||
|
import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-subscription.service';
|
||||||
@Controller('billing')
|
@Controller('billing')
|
||||||
@UseFilters(BillingRestApiExceptionFilter)
|
@UseFilters(BillingRestApiExceptionFilter)
|
||||||
export class BillingController {
|
export class BillingController {
|
||||||
|
|||||||
@ -12,5 +12,6 @@ export class BillingException extends CustomException {
|
|||||||
export enum BillingExceptionCode {
|
export enum BillingExceptionCode {
|
||||||
BILLING_CUSTOMER_NOT_FOUND = 'BILLING_CUSTOMER_NOT_FOUND',
|
BILLING_CUSTOMER_NOT_FOUND = 'BILLING_CUSTOMER_NOT_FOUND',
|
||||||
BILLING_PRODUCT_NOT_FOUND = 'BILLING_PRODUCT_NOT_FOUND',
|
BILLING_PRODUCT_NOT_FOUND = 'BILLING_PRODUCT_NOT_FOUND',
|
||||||
|
BILLING_PRICE_NOT_FOUND = 'BILLING_PRICE_NOT_FOUND',
|
||||||
BILLING_SUBSCRIPTION_EVENT_WORKSPACE_NOT_FOUND = 'BILLING_SUBSCRIPTION_EVENT_WORKSPACE_NOT_FOUND',
|
BILLING_SUBSCRIPTION_EVENT_WORKSPACE_NOT_FOUND = 'BILLING_SUBSCRIPTION_EVENT_WORKSPACE_NOT_FOUND',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,14 +14,15 @@ import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entitie
|
|||||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||||
import { BillingRestApiExceptionFilter } from 'src/engine/core-modules/billing/filters/billing-api-exception.filter';
|
import { BillingRestApiExceptionFilter } from 'src/engine/core-modules/billing/filters/billing-api-exception.filter';
|
||||||
import { BillingWorkspaceMemberListener } from 'src/engine/core-modules/billing/listeners/billing-workspace-member.listener';
|
import { BillingWorkspaceMemberListener } from 'src/engine/core-modules/billing/listeners/billing-workspace-member.listener';
|
||||||
|
import { BillingPlanService } from 'src/engine/core-modules/billing/services/billing-plan.service';
|
||||||
import { BillingPortalWorkspaceService } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service';
|
import { BillingPortalWorkspaceService } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service';
|
||||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||||
import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing/services/billing-webhook-entitlement.service';
|
|
||||||
import { BillingWebhookPriceService } from 'src/engine/core-modules/billing/services/billing-webhook-price.service';
|
|
||||||
import { BillingWebhookProductService } from 'src/engine/core-modules/billing/services/billing-webhook-product.service';
|
|
||||||
import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing/services/billing-webhook-subscription.service';
|
|
||||||
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
||||||
import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module';
|
import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module';
|
||||||
|
import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-entitlement.service';
|
||||||
|
import { BillingWebhookPriceService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-price.service';
|
||||||
|
import { BillingWebhookProductService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-product.service';
|
||||||
|
import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing/webhooks/services/billing-webhook-subscription.service';
|
||||||
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
|
import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module';
|
||||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||||
@ -56,6 +57,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|||||||
BillingWebhookEntitlementService,
|
BillingWebhookEntitlementService,
|
||||||
BillingPortalWorkspaceService,
|
BillingPortalWorkspaceService,
|
||||||
BillingResolver,
|
BillingResolver,
|
||||||
|
BillingPlanService,
|
||||||
BillingWorkspaceMemberListener,
|
BillingWorkspaceMemberListener,
|
||||||
BillingService,
|
BillingService,
|
||||||
BillingWebhookProductService,
|
BillingWebhookProductService,
|
||||||
|
|||||||
@ -1,16 +1,24 @@
|
|||||||
import { UseGuards } from '@nestjs/common';
|
import { UseGuards } from '@nestjs/common';
|
||||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||||
|
|
||||||
import { BillingSessionInput } from 'src/engine/core-modules/billing/dto/billing-session.input';
|
import { GraphQLError } from 'graphql';
|
||||||
import { CheckoutSessionInput } from 'src/engine/core-modules/billing/dto/checkout-session.input';
|
|
||||||
import { ProductPricesEntity } from 'src/engine/core-modules/billing/dto/product-prices.entity';
|
import { BillingCheckoutSessionInput } from 'src/engine/core-modules/billing/dtos/inputs/billing-checkout-session.input';
|
||||||
import { ProductInput } from 'src/engine/core-modules/billing/dto/product.input';
|
import { BillingProductInput } from 'src/engine/core-modules/billing/dtos/inputs/billing-product.input';
|
||||||
import { SessionEntity } from 'src/engine/core-modules/billing/dto/session.entity';
|
import { BillingSessionInput } from 'src/engine/core-modules/billing/dtos/inputs/billing-session.input';
|
||||||
import { UpdateBillingEntity } from 'src/engine/core-modules/billing/dto/update-billing.entity';
|
import { BillingPlanOutput } from 'src/engine/core-modules/billing/dtos/outputs/billing-plan.output';
|
||||||
|
import { BillingProductPricesOutput } from 'src/engine/core-modules/billing/dtos/outputs/billing-product-prices.output';
|
||||||
|
import { BillingSessionOutput } from 'src/engine/core-modules/billing/dtos/outputs/billing-session.output';
|
||||||
|
import { BillingUpdateOutput } from 'src/engine/core-modules/billing/dtos/outputs/billing-update.output';
|
||||||
import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum';
|
import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum';
|
||||||
|
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
|
||||||
|
import { BillingPlanService } from 'src/engine/core-modules/billing/services/billing-plan.service';
|
||||||
import { BillingPortalWorkspaceService } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service';
|
import { BillingPortalWorkspaceService } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service';
|
||||||
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service';
|
||||||
import { StripePriceService } from 'src/engine/core-modules/billing/stripe/services/stripe-price.service';
|
import { StripePriceService } from 'src/engine/core-modules/billing/stripe/services/stripe-price.service';
|
||||||
|
import { formatBillingDatabaseProductToGraphqlDTO } from 'src/engine/core-modules/billing/utils/format-database-product-to-graphql-dto.util';
|
||||||
|
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||||
|
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||||
@ -24,20 +32,26 @@ export class BillingResolver {
|
|||||||
private readonly billingSubscriptionService: BillingSubscriptionService,
|
private readonly billingSubscriptionService: BillingSubscriptionService,
|
||||||
private readonly billingPortalWorkspaceService: BillingPortalWorkspaceService,
|
private readonly billingPortalWorkspaceService: BillingPortalWorkspaceService,
|
||||||
private readonly stripePriceService: StripePriceService,
|
private readonly stripePriceService: StripePriceService,
|
||||||
|
private readonly billingPlanService: BillingPlanService,
|
||||||
|
private readonly featureFlagService: FeatureFlagService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Query(() => ProductPricesEntity)
|
@Query(() => BillingProductPricesOutput)
|
||||||
async getProductPrices(@Args() { product }: ProductInput) {
|
@UseGuards(WorkspaceAuthGuard)
|
||||||
|
async getProductPrices(
|
||||||
|
@AuthWorkspace() workspace: Workspace,
|
||||||
|
@Args() { product }: BillingProductInput,
|
||||||
|
) {
|
||||||
const productPrices =
|
const productPrices =
|
||||||
await this.stripePriceService.getStripePrices(product);
|
await this.stripePriceService.getStripePrices(product);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalNumberOfPrices: productPrices.length,
|
totalNumberOfPrices: productPrices.length,
|
||||||
productPrices: productPrices,
|
productPrices,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query(() => SessionEntity)
|
@Query(() => BillingSessionOutput)
|
||||||
@UseGuards(WorkspaceAuthGuard)
|
@UseGuards(WorkspaceAuthGuard)
|
||||||
async billingPortalSession(
|
async billingPortalSession(
|
||||||
@AuthWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
@ -51,7 +65,7 @@ export class BillingResolver {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => SessionEntity)
|
@Mutation(() => BillingSessionOutput)
|
||||||
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
|
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
|
||||||
async checkoutSession(
|
async checkoutSession(
|
||||||
@AuthWorkspace() workspace: Workspace,
|
@AuthWorkspace() workspace: Workspace,
|
||||||
@ -62,15 +76,37 @@ export class BillingResolver {
|
|||||||
successUrlPath,
|
successUrlPath,
|
||||||
plan,
|
plan,
|
||||||
requirePaymentMethod,
|
requirePaymentMethod,
|
||||||
}: CheckoutSessionInput,
|
}: BillingCheckoutSessionInput,
|
||||||
) {
|
) {
|
||||||
const productPrice = await this.stripePriceService.getStripePrice(
|
const isBillingPlansEnabled =
|
||||||
AvailableProduct.BasePlan,
|
await this.featureFlagService.isFeatureEnabled(
|
||||||
recurringInterval,
|
FeatureFlagKey.IsBillingPlansEnabled,
|
||||||
);
|
workspace.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
let productPrice;
|
||||||
|
|
||||||
|
if (isBillingPlansEnabled) {
|
||||||
|
const baseProduct = await this.billingPlanService.getPlanBaseProduct(
|
||||||
|
plan ?? BillingPlanKey.PRO,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!baseProduct) {
|
||||||
|
throw new GraphQLError('Base product not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
productPrice = baseProduct.billingPrices.find(
|
||||||
|
(price) => price.interval === recurringInterval,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
productPrice = await this.stripePriceService.getStripePrice(
|
||||||
|
AvailableProduct.BasePlan,
|
||||||
|
recurringInterval,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!productPrice) {
|
if (!productPrice) {
|
||||||
throw new Error(
|
throw new GraphQLError(
|
||||||
'Product price not found for the given recurring interval',
|
'Product price not found for the given recurring interval',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -87,11 +123,19 @@ export class BillingResolver {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => UpdateBillingEntity)
|
@Mutation(() => BillingUpdateOutput)
|
||||||
@UseGuards(WorkspaceAuthGuard)
|
@UseGuards(WorkspaceAuthGuard)
|
||||||
async updateBillingSubscription(@AuthWorkspace() workspace: Workspace) {
|
async updateBillingSubscription(@AuthWorkspace() workspace: Workspace) {
|
||||||
await this.billingSubscriptionService.applyBillingSubscription(workspace);
|
await this.billingSubscriptionService.applyBillingSubscription(workspace);
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Query(() => [BillingPlanOutput])
|
||||||
|
@UseGuards(WorkspaceAuthGuard)
|
||||||
|
async plans(): Promise<BillingPlanOutput[]> {
|
||||||
|
const plans = await this.billingPlanService.getPlans();
|
||||||
|
|
||||||
|
return plans.map(formatBillingDatabaseProductToGraphqlDTO);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,9 +15,9 @@ import { StripeBillingMeterService } from 'src/engine/core-modules/billing/strip
|
|||||||
import { StripePriceService } from 'src/engine/core-modules/billing/stripe/services/stripe-price.service';
|
import { StripePriceService } from 'src/engine/core-modules/billing/stripe/services/stripe-price.service';
|
||||||
import { StripeProductService } from 'src/engine/core-modules/billing/stripe/services/stripe-product.service';
|
import { StripeProductService } from 'src/engine/core-modules/billing/stripe/services/stripe-product.service';
|
||||||
import { isStripeValidProductMetadata } from 'src/engine/core-modules/billing/utils/is-stripe-valid-product-metadata.util';
|
import { isStripeValidProductMetadata } from 'src/engine/core-modules/billing/utils/is-stripe-valid-product-metadata.util';
|
||||||
import { transformStripeMeterDataToMeterRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-meter-data-to-meter-repository-data.util';
|
import { transformStripeMeterToDatabaseMeter } from 'src/engine/core-modules/billing/utils/transform-stripe-meter-to-database-meter.util';
|
||||||
import { transformStripePriceDataToPriceRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-price-data-to-price-repository-data.util';
|
import { transformStripePriceToDatabasePrice } from 'src/engine/core-modules/billing/utils/transform-stripe-price-to-database-price.util';
|
||||||
import { transformStripeProductDataToProductRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-product-data-to-product-repository-data.util';
|
import { transformStripeProductToDatabaseProduct } from 'src/engine/core-modules/billing/utils/transform-stripe-product-to-database-product.util';
|
||||||
@Command({
|
@Command({
|
||||||
name: 'billing:sync-plans-data',
|
name: 'billing:sync-plans-data',
|
||||||
description:
|
description:
|
||||||
@ -47,7 +47,7 @@ export class BillingSyncPlansDataCommand extends BaseCommandRunner {
|
|||||||
try {
|
try {
|
||||||
if (!options.dryRun) {
|
if (!options.dryRun) {
|
||||||
await this.billingMeterRepository.upsert(
|
await this.billingMeterRepository.upsert(
|
||||||
transformStripeMeterDataToMeterRepositoryData(meter),
|
transformStripeMeterToDatabaseMeter(meter),
|
||||||
{
|
{
|
||||||
conflictPaths: ['stripeMeterId'],
|
conflictPaths: ['stripeMeterId'],
|
||||||
},
|
},
|
||||||
@ -67,7 +67,7 @@ export class BillingSyncPlansDataCommand extends BaseCommandRunner {
|
|||||||
try {
|
try {
|
||||||
if (!options.dryRun) {
|
if (!options.dryRun) {
|
||||||
await this.billingProductRepository.upsert(
|
await this.billingProductRepository.upsert(
|
||||||
transformStripeProductDataToProductRepositoryData(product),
|
transformStripeProductToDatabaseProduct(product),
|
||||||
{
|
{
|
||||||
conflictPaths: ['stripeProductId'],
|
conflictPaths: ['stripeProductId'],
|
||||||
},
|
},
|
||||||
@ -148,9 +148,7 @@ export class BillingSyncPlansDataCommand extends BaseCommandRunner {
|
|||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
const transformedPrices = billingPrices.flatMap((prices) =>
|
const transformedPrices = billingPrices.flatMap((prices) =>
|
||||||
prices.map((price) =>
|
prices.map((price) => transformStripePriceToDatabasePrice(price)),
|
||||||
transformStripePriceDataToPriceRepositoryData(price),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.logger.log(`Upserting ${transformedPrices.length} transformed prices`);
|
this.logger.log(`Upserting ${transformedPrices.length} transformed prices`);
|
||||||
|
|||||||
@ -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';
|
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class ProductPriceEntity {
|
export class BillingProductPriceDTO {
|
||||||
@Field(() => SubscriptionInterval)
|
@Field(() => SubscriptionInterval)
|
||||||
recurringInterval: Stripe.Price.Recurring.Interval;
|
recurringInterval: Stripe.Price.Recurring.Interval;
|
||||||
|
|
||||||
@ -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';
|
import { Min } from 'class-validator';
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class TrialPeriodDTO {
|
export class BillingTrialPeriodDTO {
|
||||||
@Field(() => Number)
|
@Field(() => Number)
|
||||||
@Min(0)
|
@Min(0)
|
||||||
duration: number;
|
duration: number;
|
||||||
@ -12,7 +12,7 @@ import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-pl
|
|||||||
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
||||||
|
|
||||||
@ArgsType()
|
@ArgsType()
|
||||||
export class CheckoutSessionInput {
|
export class BillingCheckoutSessionInput {
|
||||||
@Field(() => SubscriptionInterval)
|
@Field(() => SubscriptionInterval)
|
||||||
@IsEnum(SubscriptionInterval)
|
@IsEnum(SubscriptionInterval)
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@ -5,7 +5,7 @@ import { IsNotEmpty, IsString } from 'class-validator';
|
|||||||
import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum';
|
import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum';
|
||||||
|
|
||||||
@ArgsType()
|
@ArgsType()
|
||||||
export class ProductInput {
|
export class BillingProductInput {
|
||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@ -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';
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class SessionEntity {
|
export class BillingSessionOutput {
|
||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { Field, ObjectType } from '@nestjs/graphql';
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class UpdateBillingEntity {
|
export class BillingUpdateOutput {
|
||||||
@Field(() => Boolean, {
|
@Field(() => Boolean, {
|
||||||
description: 'Boolean that confirms query was successful',
|
description: 'Boolean that confirms query was successful',
|
||||||
})
|
})
|
||||||
@ -32,8 +32,8 @@ export class BillingProduct {
|
|||||||
@Column({ nullable: false })
|
@Column({ nullable: false })
|
||||||
active: boolean;
|
active: boolean;
|
||||||
|
|
||||||
@Column({ nullable: true, type: 'text' })
|
@Column({ nullable: false, type: 'text', default: '' })
|
||||||
description: string | null;
|
description: string;
|
||||||
|
|
||||||
@Column({ nullable: false })
|
@Column({ nullable: false })
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@ -1,4 +1,10 @@
|
|||||||
|
import { registerEnumType } from '@nestjs/graphql';
|
||||||
|
|
||||||
export enum BillingPriceTiersMode {
|
export enum BillingPriceTiersMode {
|
||||||
GRADUATED = 'GRADUATED',
|
GRADUATED = 'GRADUATED',
|
||||||
VOLUME = 'VOLUME',
|
VOLUME = 'VOLUME',
|
||||||
}
|
}
|
||||||
|
registerEnumType(BillingPriceTiersMode, {
|
||||||
|
name: 'BillingPriceTiersMode',
|
||||||
|
description: 'The different billing price tiers modes',
|
||||||
|
});
|
||||||
|
|||||||
@ -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 Stripe from 'stripe';
|
||||||
import { Not, Repository } from 'typeorm';
|
import { Not, Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import {
|
||||||
|
BillingException,
|
||||||
|
BillingExceptionCode,
|
||||||
|
} from 'src/engine/core-modules/billing/billing.exception';
|
||||||
import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
|
import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
|
||||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||||
import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum';
|
import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum';
|
||||||
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
|
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
|
||||||
|
import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-plan-key.enum';
|
||||||
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
||||||
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
|
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
|
||||||
|
import { BillingPlanService } from 'src/engine/core-modules/billing/services/billing-plan.service';
|
||||||
import { StripePriceService } from 'src/engine/core-modules/billing/stripe/services/stripe-price.service';
|
import { StripePriceService } from 'src/engine/core-modules/billing/stripe/services/stripe-price.service';
|
||||||
import { StripeSubscriptionItemService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service';
|
import { StripeSubscriptionItemService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service';
|
||||||
import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service';
|
import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service';
|
||||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||||
|
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||||
|
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BillingSubscriptionService {
|
export class BillingSubscriptionService {
|
||||||
protected readonly logger = new Logger(BillingSubscriptionService.name);
|
protected readonly logger = new Logger(BillingSubscriptionService.name);
|
||||||
@ -25,7 +32,9 @@ export class BillingSubscriptionService {
|
|||||||
private readonly stripeSubscriptionService: StripeSubscriptionService,
|
private readonly stripeSubscriptionService: StripeSubscriptionService,
|
||||||
private readonly stripePriceService: StripePriceService,
|
private readonly stripePriceService: StripePriceService,
|
||||||
private readonly stripeSubscriptionItemService: StripeSubscriptionItemService,
|
private readonly stripeSubscriptionItemService: StripeSubscriptionItemService,
|
||||||
|
private readonly billingPlanService: BillingPlanService,
|
||||||
private readonly environmentService: EnvironmentService,
|
private readonly environmentService: EnvironmentService,
|
||||||
|
private readonly featureFlagService: FeatureFlagService,
|
||||||
@InjectRepository(BillingEntitlement, 'core')
|
@InjectRepository(BillingEntitlement, 'core')
|
||||||
private readonly billingEntitlementRepository: Repository<BillingEntitlement>,
|
private readonly billingEntitlementRepository: Repository<BillingEntitlement>,
|
||||||
@InjectRepository(BillingSubscription, 'core')
|
@InjectRepository(BillingSubscription, 'core')
|
||||||
@ -56,19 +65,37 @@ export class BillingSubscriptionService {
|
|||||||
'BILLING_STRIPE_BASE_PLAN_PRODUCT_ID',
|
'BILLING_STRIPE_BASE_PLAN_PRODUCT_ID',
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
|
const isBillingPlansEnabled =
|
||||||
|
await this.featureFlagService.isFeatureEnabled(
|
||||||
|
FeatureFlagKey.IsBillingPlansEnabled,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
const billingSubscription = await this.getCurrentBillingSubscriptionOrThrow(
|
const billingSubscription = await this.getCurrentBillingSubscriptionOrThrow(
|
||||||
{ workspaceId },
|
{ workspaceId },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getStripeProductId = isBillingPlansEnabled
|
||||||
|
? (await this.billingPlanService.getPlanBaseProduct(BillingPlanKey.PRO))
|
||||||
|
?.stripeProductId
|
||||||
|
: stripeProductId;
|
||||||
|
|
||||||
|
if (!getStripeProductId) {
|
||||||
|
throw new BillingException(
|
||||||
|
'Base product not found',
|
||||||
|
BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const billingSubscriptionItem =
|
const billingSubscriptionItem =
|
||||||
billingSubscription.billingSubscriptionItems.filter(
|
billingSubscription.billingSubscriptionItems.filter(
|
||||||
(billingSubscriptionItem) =>
|
(billingSubscriptionItem) =>
|
||||||
billingSubscriptionItem.stripeProductId === stripeProductId,
|
billingSubscriptionItem.stripeProductId === getStripeProductId,
|
||||||
)?.[0];
|
)?.[0];
|
||||||
|
|
||||||
if (!billingSubscriptionItem) {
|
if (!billingSubscriptionItem) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Cannot find billingSubscriptionItem for product ${stripeProductId} for workspace ${workspaceId}`,
|
`Cannot find billingSubscriptionItem for product ${getStripeProductId} for workspace ${workspaceId}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -127,7 +154,11 @@ export class BillingSubscriptionService {
|
|||||||
const billingSubscription = await this.getCurrentBillingSubscriptionOrThrow(
|
const billingSubscription = await this.getCurrentBillingSubscriptionOrThrow(
|
||||||
{ workspaceId: workspace.id },
|
{ workspaceId: workspace.id },
|
||||||
);
|
);
|
||||||
|
const isBillingPlansEnabled =
|
||||||
|
await this.featureFlagService.isFeatureEnabled(
|
||||||
|
FeatureFlagKey.IsBillingPlansEnabled,
|
||||||
|
workspace.id,
|
||||||
|
);
|
||||||
const newInterval =
|
const newInterval =
|
||||||
billingSubscription?.interval === SubscriptionInterval.Year
|
billingSubscription?.interval === SubscriptionInterval.Year
|
||||||
? SubscriptionInterval.Month
|
? SubscriptionInterval.Month
|
||||||
@ -136,10 +167,29 @@ export class BillingSubscriptionService {
|
|||||||
const billingSubscriptionItem =
|
const billingSubscriptionItem =
|
||||||
await this.getCurrentBillingSubscriptionItemOrThrow(workspace.id);
|
await this.getCurrentBillingSubscriptionItemOrThrow(workspace.id);
|
||||||
|
|
||||||
const productPrice = await this.stripePriceService.getStripePrice(
|
let productPrice;
|
||||||
AvailableProduct.BasePlan,
|
|
||||||
newInterval,
|
if (isBillingPlansEnabled) {
|
||||||
);
|
const baseProduct = await this.billingPlanService.getPlanBaseProduct(
|
||||||
|
BillingPlanKey.PRO,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!baseProduct) {
|
||||||
|
throw new BillingException(
|
||||||
|
'Base product not found',
|
||||||
|
BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
productPrice = baseProduct.billingPrices.find(
|
||||||
|
(price) => price.interval === newInterval,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
productPrice = await this.stripePriceService.getStripePrice(
|
||||||
|
AvailableProduct.BasePlan,
|
||||||
|
newInterval,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!productPrice) {
|
if (!productPrice) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
|
|||||||
|
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
import { ProductPriceEntity } from 'src/engine/core-modules/billing/dto/product-price.entity';
|
import { BillingProductPriceDTO } from 'src/engine/core-modules/billing/dtos/billing-product-price.dto';
|
||||||
import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum';
|
import { AvailableProduct } from 'src/engine/core-modules/billing/enums/billing-available-product.enum';
|
||||||
import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service';
|
import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service';
|
||||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||||
@ -46,10 +46,10 @@ export class StripePriceService {
|
|||||||
if (product === AvailableProduct.BasePlan) {
|
if (product === AvailableProduct.BasePlan) {
|
||||||
return this.environmentService.get('BILLING_STRIPE_BASE_PLAN_PRODUCT_ID');
|
return this.environmentService.get('BILLING_STRIPE_BASE_PLAN_PRODUCT_ID');
|
||||||
}
|
}
|
||||||
} // PD:,will be eliminated after refactoring
|
}
|
||||||
|
|
||||||
formatProductPrices(prices: Stripe.Price[]): ProductPriceEntity[] {
|
formatProductPrices(prices: Stripe.Price[]): BillingProductPriceDTO[] {
|
||||||
const productPrices: ProductPriceEntity[] = Object.values(
|
const productPrices: BillingProductPriceDTO[] = Object.values(
|
||||||
prices
|
prices
|
||||||
.filter((item) => item.recurring?.interval && item.unit_amount)
|
.filter((item) => item.recurring?.interval && item.unit_amount)
|
||||||
.reduce((acc, item: Stripe.Price) => {
|
.reduce((acc, item: Stripe.Price) => {
|
||||||
@ -68,7 +68,7 @@ export class StripePriceService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc satisfies Record<string, ProductPriceEntity>;
|
return acc satisfies Record<string, BillingProductPriceDTO>;
|
||||||
}, {}),
|
}, {}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -76,8 +76,10 @@ export class StripePriceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getPricesByProductId(productId: string) {
|
async getPricesByProductId(productId: string) {
|
||||||
const prices = await this.stripe.prices.search({
|
const prices = await this.stripe.prices.list({
|
||||||
query: `product:'${productId}'`,
|
product: productId,
|
||||||
|
type: 'recurring',
|
||||||
|
expand: ['data.currency_options', 'data.tiers'],
|
||||||
});
|
});
|
||||||
|
|
||||||
return prices.data;
|
return prices.data;
|
||||||
|
|||||||
@ -23,7 +23,10 @@ export class StripeProductService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getAllProducts() {
|
async getAllProducts() {
|
||||||
const products = await this.stripe.products.list();
|
const products = await this.stripe.products.list({
|
||||||
|
active: true,
|
||||||
|
limit: 100,
|
||||||
|
});
|
||||||
|
|
||||||
return products.data;
|
return products.data;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
planKey: BillingPlanKey;
|
||||||
priceUsageBased: BillingUsageType;
|
priceUsageBased: BillingUsageType;
|
||||||
|
isBaseProduct: 'true' | 'false';
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
}
|
}
|
||||||
| Record<string, never>;
|
| 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 = {
|
const metadata: Stripe.Metadata = {
|
||||||
planKey: BillingPlanKey.PRO,
|
planKey: BillingPlanKey.PRO,
|
||||||
priceUsageBased: BillingUsageType.METERED,
|
priceUsageBased: BillingUsageType.METERED,
|
||||||
|
isBaseProduct: 'true',
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(isStripeValidProductMetadata(metadata)).toBe(true);
|
expect(isStripeValidProductMetadata(metadata)).toBe(true);
|
||||||
@ -22,6 +23,7 @@ describe('isStripeValidProductMetadata', () => {
|
|||||||
const metadata: Stripe.Metadata = {
|
const metadata: Stripe.Metadata = {
|
||||||
planKey: BillingPlanKey.ENTERPRISE,
|
planKey: BillingPlanKey.ENTERPRISE,
|
||||||
priceUsageBased: BillingUsageType.METERED,
|
priceUsageBased: BillingUsageType.METERED,
|
||||||
|
isBaseProduct: 'false',
|
||||||
randomKey: 'randomValue',
|
randomKey: 'randomValue',
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -32,6 +34,7 @@ describe('isStripeValidProductMetadata', () => {
|
|||||||
const metadata: Stripe.Metadata = {
|
const metadata: Stripe.Metadata = {
|
||||||
planKey: 'invalid',
|
planKey: 'invalid',
|
||||||
priceUsageBased: BillingUsageType.METERED,
|
priceUsageBased: BillingUsageType.METERED,
|
||||||
|
isBaseProduct: 'invalid',
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(isStripeValidProductMetadata(metadata)).toBe(false);
|
expect(isStripeValidProductMetadata(metadata)).toBe(false);
|
||||||
@ -41,6 +44,7 @@ describe('isStripeValidProductMetadata', () => {
|
|||||||
const metadata: Stripe.Metadata = {
|
const metadata: Stripe.Metadata = {
|
||||||
planKey: BillingPlanKey.PRO,
|
planKey: BillingPlanKey.PRO,
|
||||||
priceUsageBased: 'invalid',
|
priceUsageBased: 'invalid',
|
||||||
|
isBaseProduct: 'true',
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(isStripeValidProductMetadata(metadata)).toBe(false);
|
expect(isStripeValidProductMetadata(metadata)).toBe(false);
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import Stripe from 'stripe';
|
|||||||
|
|
||||||
import { BillingMeterEventTimeWindow } from 'src/engine/core-modules/billing/enums/billing-meter-event-time-window.enum';
|
import { BillingMeterEventTimeWindow } from 'src/engine/core-modules/billing/enums/billing-meter-event-time-window.enum';
|
||||||
import { BillingMeterStatus } from 'src/engine/core-modules/billing/enums/billing-meter-status.enum';
|
import { BillingMeterStatus } from 'src/engine/core-modules/billing/enums/billing-meter-status.enum';
|
||||||
import { transformStripeMeterDataToMeterRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-meter-data-to-meter-repository-data.util';
|
import { transformStripeMeterToDatabaseMeter } from 'src/engine/core-modules/billing/utils/transform-stripe-meter-to-database-meter.util';
|
||||||
|
|
||||||
describe('transformStripeMeterDataToMeterRepositoryData', () => {
|
describe('transformStripeMeterDataToMeterRepositoryData', () => {
|
||||||
it('should return the correct data with customer mapping', () => {
|
it('should return the correct data with customer mapping', () => {
|
||||||
@ -31,7 +31,7 @@ describe('transformStripeMeterDataToMeterRepositoryData', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = transformStripeMeterDataToMeterRepositoryData(data);
|
const result = transformStripeMeterToDatabaseMeter(data);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
stripeMeterId: 'met_123',
|
stripeMeterId: 'met_123',
|
||||||
@ -74,7 +74,7 @@ describe('transformStripeMeterDataToMeterRepositoryData', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = transformStripeMeterDataToMeterRepositoryData(data);
|
const result = transformStripeMeterToDatabaseMeter(data);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
stripeMeterId: 'met_1234',
|
stripeMeterId: 'met_1234',
|
||||||
@ -6,8 +6,8 @@ import { BillingPriceTiersMode } from 'src/engine/core-modules/billing/enums/bil
|
|||||||
import { BillingPriceType } from 'src/engine/core-modules/billing/enums/billing-price-type.enum';
|
import { BillingPriceType } from 'src/engine/core-modules/billing/enums/billing-price-type.enum';
|
||||||
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
||||||
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
||||||
import { transformStripePriceDataToPriceRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-price-data-to-price-repository-data.util';
|
import { transformStripePriceToDatabasePrice } from 'src/engine/core-modules/billing/utils/transform-stripe-price-to-database-price.util';
|
||||||
describe('transformStripePriceDataToPriceRepositoryData', () => {
|
describe('transformStripePriceToDatabasePrice', () => {
|
||||||
const createMockPrice = (overrides = {}): Stripe.Price =>
|
const createMockPrice = (overrides = {}): Stripe.Price =>
|
||||||
({
|
({
|
||||||
id: 'price_123',
|
id: 'price_123',
|
||||||
@ -34,7 +34,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
|
|||||||
|
|
||||||
it('should transform basic price data correctly', () => {
|
it('should transform basic price data correctly', () => {
|
||||||
const mockPrice = createMockPrice();
|
const mockPrice = createMockPrice();
|
||||||
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
|
const result = transformStripePriceToDatabasePrice(mockPrice);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
stripePriceId: 'price_123',
|
stripePriceId: 'price_123',
|
||||||
@ -73,7 +73,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
|
|||||||
const mockPrice = createMockPrice({
|
const mockPrice = createMockPrice({
|
||||||
tax_behavior: stripeTaxBehavior as Stripe.Price.TaxBehavior,
|
tax_behavior: stripeTaxBehavior as Stripe.Price.TaxBehavior,
|
||||||
});
|
});
|
||||||
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
|
const result = transformStripePriceToDatabasePrice(mockPrice);
|
||||||
|
|
||||||
expect(result.taxBehavior).toBe(expected);
|
expect(result.taxBehavior).toBe(expected);
|
||||||
},
|
},
|
||||||
@ -88,7 +88,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
|
|||||||
const mockPrice = createMockPrice({
|
const mockPrice = createMockPrice({
|
||||||
type: stripeType as Stripe.Price.Type,
|
type: stripeType as Stripe.Price.Type,
|
||||||
});
|
});
|
||||||
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
|
const result = transformStripePriceToDatabasePrice(mockPrice);
|
||||||
|
|
||||||
expect(result.type).toBe(expected);
|
expect(result.type).toBe(expected);
|
||||||
});
|
});
|
||||||
@ -104,7 +104,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
|
|||||||
const mockPrice = createMockPrice({
|
const mockPrice = createMockPrice({
|
||||||
billing_scheme: stripeScheme as Stripe.Price.BillingScheme,
|
billing_scheme: stripeScheme as Stripe.Price.BillingScheme,
|
||||||
});
|
});
|
||||||
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
|
const result = transformStripePriceToDatabasePrice(mockPrice);
|
||||||
|
|
||||||
expect(result.billingScheme).toBe(expected);
|
expect(result.billingScheme).toBe(expected);
|
||||||
},
|
},
|
||||||
@ -120,7 +120,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
|
|||||||
meter: 'meter_123',
|
meter: 'meter_123',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
|
const result = transformStripePriceToDatabasePrice(mockPrice);
|
||||||
|
|
||||||
expect(result.stripeMeterId).toBe('meter_123');
|
expect(result.stripeMeterId).toBe('meter_123');
|
||||||
expect(result.usageType).toBe(BillingUsageType.METERED);
|
expect(result.usageType).toBe(BillingUsageType.METERED);
|
||||||
@ -139,7 +139,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
|
|||||||
meter: null,
|
meter: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
|
const result = transformStripePriceToDatabasePrice(mockPrice);
|
||||||
|
|
||||||
expect(result.interval).toBe(expected);
|
expect(result.interval).toBe(expected);
|
||||||
});
|
});
|
||||||
@ -162,7 +162,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
|
|||||||
tiers: mockTiers,
|
tiers: mockTiers,
|
||||||
tiers_mode: stripeTiersMode as Stripe.Price.TiersMode,
|
tiers_mode: stripeTiersMode as Stripe.Price.TiersMode,
|
||||||
});
|
});
|
||||||
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
|
const result = transformStripePriceToDatabasePrice(mockPrice);
|
||||||
|
|
||||||
expect(result.tiersMode).toBe(expected);
|
expect(result.tiersMode).toBe(expected);
|
||||||
expect(result.tiers).toEqual(mockTiers);
|
expect(result.tiers).toEqual(mockTiers);
|
||||||
@ -179,7 +179,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
|
|||||||
const mockPrice = createMockPrice({
|
const mockPrice = createMockPrice({
|
||||||
transform_quantity: transformQuantity,
|
transform_quantity: transformQuantity,
|
||||||
});
|
});
|
||||||
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
|
const result = transformStripePriceToDatabasePrice(mockPrice);
|
||||||
|
|
||||||
expect(result.transformQuantity).toEqual(transformQuantity);
|
expect(result.transformQuantity).toEqual(transformQuantity);
|
||||||
});
|
});
|
||||||
@ -192,7 +192,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
const mockPrice = createMockPrice({ currency_options: currencyOptions });
|
const mockPrice = createMockPrice({ currency_options: currencyOptions });
|
||||||
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
|
const result = transformStripePriceToDatabasePrice(mockPrice);
|
||||||
|
|
||||||
expect(result.currencyOptions).toEqual(currencyOptions);
|
expect(result.currencyOptions).toEqual(currencyOptions);
|
||||||
});
|
});
|
||||||
@ -206,7 +206,7 @@ describe('transformStripePriceDataToPriceRepositoryData', () => {
|
|||||||
tiers: null,
|
tiers: null,
|
||||||
currency_options: null,
|
currency_options: null,
|
||||||
});
|
});
|
||||||
const result = transformStripePriceDataToPriceRepositoryData(mockPrice);
|
const result = transformStripePriceToDatabasePrice(mockPrice);
|
||||||
|
|
||||||
expect(result.nickname).toBeUndefined();
|
expect(result.nickname).toBeUndefined();
|
||||||
expect(result.unitAmount).toBeUndefined();
|
expect(result.unitAmount).toBeUndefined();
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
import { transformStripeProductDataToProductRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-product-data-to-product-repository-data.util';
|
import { transformStripeProductToDatabaseProduct } from 'src/engine/core-modules/billing/utils/transform-stripe-product-to-database-product.util';
|
||||||
describe('transformStripeProductDataToProductRepositoryData', () => {
|
describe('transformStripeProductToDatabaseProduct', () => {
|
||||||
it('should return the correct data', () => {
|
it('should return the correct data', () => {
|
||||||
const data: Stripe.Product = {
|
const data: Stripe.Product = {
|
||||||
id: 'prod_123',
|
id: 'prod_123',
|
||||||
@ -28,7 +28,7 @@ describe('transformStripeProductDataToProductRepositoryData', () => {
|
|||||||
metadata: { key: 'value' },
|
metadata: { key: 'value' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = transformStripeProductDataToProductRepositoryData(data);
|
const result = transformStripeProductToDatabaseProduct(data);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
stripeProductId: 'prod_123',
|
stripeProductId: 'prod_123',
|
||||||
@ -67,7 +67,7 @@ describe('transformStripeProductDataToProductRepositoryData', () => {
|
|||||||
metadata: {},
|
metadata: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = transformStripeProductDataToProductRepositoryData(data);
|
const result = transformStripeProductToDatabaseProduct(data);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
stripeProductId: 'prod_456',
|
stripeProductId: 'prod_456',
|
||||||
@ -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 hasBillingPlanKey = isValidBillingPlanKey(metadata.planKey);
|
||||||
const hasPriceUsageBased = isValidPriceUsageBased(metadata.priceUsageBased);
|
const hasPriceUsageBased = isValidPriceUsageBased(metadata.priceUsageBased);
|
||||||
|
const hasIsBaseProduct =
|
||||||
|
metadata.isBaseProduct === 'true' || metadata.isBaseProduct === 'false';
|
||||||
|
|
||||||
return hasBillingPlanKey && hasPriceUsageBased;
|
return hasBillingPlanKey && hasPriceUsageBased && hasIsBaseProduct;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValidBillingPlanKey = (planKey?: string) => {
|
const isValidBillingPlanKey = (planKey?: string) => {
|
||||||
|
|||||||
@ -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 { BillingMeterEventTimeWindow } from 'src/engine/core-modules/billing/enums/billing-meter-event-time-window.enum';
|
||||||
import { BillingMeterStatus } from 'src/engine/core-modules/billing/enums/billing-meter-status.enum';
|
import { BillingMeterStatus } from 'src/engine/core-modules/billing/enums/billing-meter-status.enum';
|
||||||
|
|
||||||
export const transformStripeMeterDataToMeterRepositoryData = (
|
export const transformStripeMeterToDatabaseMeter = (
|
||||||
data: Stripe.Billing.Meter,
|
data: Stripe.Billing.Meter,
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
@ -7,9 +7,7 @@ import { BillingPriceType } from 'src/engine/core-modules/billing/enums/billing-
|
|||||||
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
||||||
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
||||||
|
|
||||||
export const transformStripePriceDataToPriceRepositoryData = (
|
export const transformStripePriceToDatabasePrice = (data: Stripe.Price) => {
|
||||||
data: Stripe.Price,
|
|
||||||
) => {
|
|
||||||
return {
|
return {
|
||||||
stripePriceId: data.id,
|
stripePriceId: data.id,
|
||||||
active: data.active,
|
active: data.active,
|
||||||
@ -1,13 +1,13 @@
|
|||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
export const transformStripeProductDataToProductRepositoryData = (
|
export const transformStripeProductToDatabaseProduct = (
|
||||||
data: Stripe.Product,
|
data: Stripe.Product,
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
stripeProductId: data.id,
|
stripeProductId: data.id,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
active: data.active,
|
active: data.active,
|
||||||
description: data.description,
|
description: data.description ?? '',
|
||||||
images: data.images,
|
images: data.images,
|
||||||
marketingFeatures: data.marketing_features,
|
marketingFeatures: data.marketing_features,
|
||||||
defaultStripePriceId: data.default_price
|
defaultStripePriceId: data.default_price
|
||||||
@ -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';
|
} from 'src/engine/core-modules/billing/billing.exception';
|
||||||
import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
|
import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity';
|
||||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||||
import { transformStripeEntitlementUpdatedEventToEntitlementRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-entitlement-updated-event-to-entitlement-repository-data.util';
|
import { transformStripeEntitlementUpdatedEventToDatabaseEntitlement } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-entitlement-updated-event-to-database-entitlement.util';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BillingWebhookEntitlementService {
|
export class BillingWebhookEntitlementService {
|
||||||
protected readonly logger = new Logger(BillingWebhookEntitlementService.name);
|
protected readonly logger = new Logger(BillingWebhookEntitlementService.name);
|
||||||
@ -39,7 +39,7 @@ export class BillingWebhookEntitlementService {
|
|||||||
const workspaceId = billingSubscription.workspaceId;
|
const workspaceId = billingSubscription.workspaceId;
|
||||||
|
|
||||||
await this.billingEntitlementRepository.upsert(
|
await this.billingEntitlementRepository.upsert(
|
||||||
transformStripeEntitlementUpdatedEventToEntitlementRepositoryData(
|
transformStripeEntitlementUpdatedEventToDatabaseEntitlement(
|
||||||
workspaceId,
|
workspaceId,
|
||||||
data,
|
data,
|
||||||
),
|
),
|
||||||
@ -12,8 +12,9 @@ import { BillingMeter } from 'src/engine/core-modules/billing/entities/billing-m
|
|||||||
import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-price.entity';
|
import { BillingPrice } from 'src/engine/core-modules/billing/entities/billing-price.entity';
|
||||||
import { BillingProduct } from 'src/engine/core-modules/billing/entities/billing-product.entity';
|
import { BillingProduct } from 'src/engine/core-modules/billing/entities/billing-product.entity';
|
||||||
import { StripeBillingMeterService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-meter.service';
|
import { StripeBillingMeterService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-meter.service';
|
||||||
import { transformStripeMeterDataToMeterRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-meter-data-to-meter-repository-data.util';
|
import { transformStripeMeterToDatabaseMeter } from 'src/engine/core-modules/billing/utils/transform-stripe-meter-to-database-meter.util';
|
||||||
import { transformStripePriceEventToPriceRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-price-event-to-price-repository-data.util';
|
import { transformStripePriceEventToDatabasePrice } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-price-event-to-database-price.util';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BillingWebhookPriceService {
|
export class BillingWebhookPriceService {
|
||||||
protected readonly logger = new Logger(BillingWebhookPriceService.name);
|
protected readonly logger = new Logger(BillingWebhookPriceService.name);
|
||||||
@ -48,7 +49,7 @@ export class BillingWebhookPriceService {
|
|||||||
const meterData = await this.stripeBillingMeterService.getMeter(meterId);
|
const meterData = await this.stripeBillingMeterService.getMeter(meterId);
|
||||||
|
|
||||||
await this.billingMeterRepository.upsert(
|
await this.billingMeterRepository.upsert(
|
||||||
transformStripeMeterDataToMeterRepositoryData(meterData),
|
transformStripeMeterToDatabaseMeter(meterData),
|
||||||
{
|
{
|
||||||
conflictPaths: ['stripeMeterId'],
|
conflictPaths: ['stripeMeterId'],
|
||||||
skipUpdateIfNoValuesChanged: true,
|
skipUpdateIfNoValuesChanged: true,
|
||||||
@ -57,7 +58,7 @@ export class BillingWebhookPriceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.billingPriceRepository.upsert(
|
await this.billingPriceRepository.upsert(
|
||||||
transformStripePriceEventToPriceRepositoryData(data),
|
transformStripePriceEventToDatabasePrice(data),
|
||||||
{
|
{
|
||||||
conflictPaths: ['stripePriceId'],
|
conflictPaths: ['stripePriceId'],
|
||||||
skipUpdateIfNoValuesChanged: true,
|
skipUpdateIfNoValuesChanged: true,
|
||||||
@ -9,7 +9,7 @@ import { BillingPlanKey } from 'src/engine/core-modules/billing/enums/billing-pl
|
|||||||
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
||||||
import { BillingProductMetadata } from 'src/engine/core-modules/billing/types/billing-product-metadata.type';
|
import { BillingProductMetadata } from 'src/engine/core-modules/billing/types/billing-product-metadata.type';
|
||||||
import { isStripeValidProductMetadata } from 'src/engine/core-modules/billing/utils/is-stripe-valid-product-metadata.util';
|
import { isStripeValidProductMetadata } from 'src/engine/core-modules/billing/utils/is-stripe-valid-product-metadata.util';
|
||||||
import { transformStripeProductEventToProductRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-product-event-to-product-repository-data.util';
|
import { transformStripeProductEventToDatabaseProduct } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-product-event-to-database-product.util';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BillingWebhookProductService {
|
export class BillingWebhookProductService {
|
||||||
protected readonly logger = new Logger(BillingWebhookProductService.name);
|
protected readonly logger = new Logger(BillingWebhookProductService.name);
|
||||||
@ -24,10 +24,10 @@ export class BillingWebhookProductService {
|
|||||||
const metadata = data.object.metadata;
|
const metadata = data.object.metadata;
|
||||||
const productRepositoryData = isStripeValidProductMetadata(metadata)
|
const productRepositoryData = isStripeValidProductMetadata(metadata)
|
||||||
? {
|
? {
|
||||||
...transformStripeProductEventToProductRepositoryData(data),
|
...transformStripeProductEventToDatabaseProduct(data),
|
||||||
metadata,
|
metadata,
|
||||||
}
|
}
|
||||||
: transformStripeProductEventToProductRepositoryData(data);
|
: transformStripeProductEventToDatabaseProduct(data);
|
||||||
|
|
||||||
await this.billingProductRepository.upsert(productRepositoryData, {
|
await this.billingProductRepository.upsert(productRepositoryData, {
|
||||||
conflictPaths: ['stripeProductId'],
|
conflictPaths: ['stripeProductId'],
|
||||||
@ -10,9 +10,9 @@ import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entitie
|
|||||||
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
|
||||||
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
|
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
|
||||||
import { StripeCustomerService } from 'src/engine/core-modules/billing/stripe/services/stripe-customer.service';
|
import { StripeCustomerService } from 'src/engine/core-modules/billing/stripe/services/stripe-customer.service';
|
||||||
import { transformStripeSubscriptionEventToCustomerRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-customer-repository-data.util';
|
import { transformStripeSubscriptionEventToDatabaseCustomer } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-customer.util';
|
||||||
import { transformStripeSubscriptionEventToSubscriptionItemRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-item-repository-data.util';
|
import { transformStripeSubscriptionEventToDatabaseSubscriptionItem } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-subscription-item.util';
|
||||||
import { transformStripeSubscriptionEventToSubscriptionRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-repository-data.util';
|
import { transformStripeSubscriptionEventToDatabaseSubscription } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-subscription.util';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BillingWebhookSubscriptionService {
|
export class BillingWebhookSubscriptionService {
|
||||||
@ -47,10 +47,7 @@ export class BillingWebhookSubscriptionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.billingCustomerRepository.upsert(
|
await this.billingCustomerRepository.upsert(
|
||||||
transformStripeSubscriptionEventToCustomerRepositoryData(
|
transformStripeSubscriptionEventToDatabaseCustomer(workspaceId, data),
|
||||||
workspaceId,
|
|
||||||
data,
|
|
||||||
),
|
|
||||||
{
|
{
|
||||||
conflictPaths: ['workspaceId'],
|
conflictPaths: ['workspaceId'],
|
||||||
skipUpdateIfNoValuesChanged: true,
|
skipUpdateIfNoValuesChanged: true,
|
||||||
@ -58,10 +55,7 @@ export class BillingWebhookSubscriptionService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await this.billingSubscriptionRepository.upsert(
|
await this.billingSubscriptionRepository.upsert(
|
||||||
transformStripeSubscriptionEventToSubscriptionRepositoryData(
|
transformStripeSubscriptionEventToDatabaseSubscription(workspaceId, data),
|
||||||
workspaceId,
|
|
||||||
data,
|
|
||||||
),
|
|
||||||
{
|
{
|
||||||
conflictPaths: ['stripeSubscriptionId'],
|
conflictPaths: ['stripeSubscriptionId'],
|
||||||
skipUpdateIfNoValuesChanged: true,
|
skipUpdateIfNoValuesChanged: true,
|
||||||
@ -74,7 +68,7 @@ export class BillingWebhookSubscriptionService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await this.billingSubscriptionItemRepository.upsert(
|
await this.billingSubscriptionItemRepository.upsert(
|
||||||
transformStripeSubscriptionEventToSubscriptionItemRepositoryData(
|
transformStripeSubscriptionEventToDatabaseSubscriptionItem(
|
||||||
billingSubscription.id,
|
billingSubscription.id,
|
||||||
data,
|
data,
|
||||||
),
|
),
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
|
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
|
||||||
import { transformStripeEntitlementUpdatedEventToEntitlementRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-entitlement-updated-event-to-entitlement-repository-data.util';
|
import { transformStripeEntitlementUpdatedEventToDatabaseEntitlement } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-entitlement-updated-event-to-database-entitlement.util';
|
||||||
|
|
||||||
describe('transformStripeEntitlementUpdatedEventToEntitlementRepositoryData', () => {
|
describe('transformStripeEntitlementUpdatedEventToDatabaseEntitlement', () => {
|
||||||
it('should return the SSO key with true value', () => {
|
it('should return the SSO key with true value', () => {
|
||||||
const data: Stripe.EntitlementsActiveEntitlementSummaryUpdatedEvent.Data = {
|
const data: Stripe.EntitlementsActiveEntitlementSummaryUpdatedEvent.Data = {
|
||||||
object: {
|
object: {
|
||||||
@ -27,11 +27,10 @@ describe('transformStripeEntitlementUpdatedEventToEntitlementRepositoryData', ()
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result =
|
const result = transformStripeEntitlementUpdatedEventToDatabaseEntitlement(
|
||||||
transformStripeEntitlementUpdatedEventToEntitlementRepositoryData(
|
'workspaceId',
|
||||||
'workspaceId',
|
data,
|
||||||
data,
|
);
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
@ -66,11 +65,10 @@ describe('transformStripeEntitlementUpdatedEventToEntitlementRepositoryData', ()
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result =
|
const result = transformStripeEntitlementUpdatedEventToDatabaseEntitlement(
|
||||||
transformStripeEntitlementUpdatedEventToEntitlementRepositoryData(
|
'workspaceId',
|
||||||
'workspaceId',
|
data,
|
||||||
data,
|
);
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{
|
{
|
||||||
@ -4,9 +4,9 @@ import { BillingPriceTiersMode } from 'src/engine/core-modules/billing/enums/bil
|
|||||||
import { BillingPriceType } from 'src/engine/core-modules/billing/enums/billing-price-type.enum';
|
import { BillingPriceType } from 'src/engine/core-modules/billing/enums/billing-price-type.enum';
|
||||||
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
||||||
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
||||||
import { transformStripePriceEventToPriceRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-price-event-to-price-repository-data.util';
|
import { transformStripePriceEventToDatabasePrice } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-price-event-to-database-price.util';
|
||||||
|
|
||||||
describe('transformStripePriceEventToPriceRepositoryData', () => {
|
describe('transformStripePriceEventToDatabasePrice', () => {
|
||||||
const createMockPriceData = (overrides = {}) => ({
|
const createMockPriceData = (overrides = {}) => ({
|
||||||
object: {
|
object: {
|
||||||
id: 'price_123',
|
id: 'price_123',
|
||||||
@ -34,9 +34,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
|
|||||||
|
|
||||||
it('should transform basic price data correctly', () => {
|
it('should transform basic price data correctly', () => {
|
||||||
const mockData = createMockPriceData();
|
const mockData = createMockPriceData();
|
||||||
const result = transformStripePriceEventToPriceRepositoryData(
|
const result = transformStripePriceEventToDatabasePrice(mockData as any);
|
||||||
mockData as any,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
stripePriceId: 'price_123',
|
stripePriceId: 'price_123',
|
||||||
@ -74,9 +72,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
|
|||||||
const mockData = createMockPriceData({
|
const mockData = createMockPriceData({
|
||||||
tax_behavior: stripeTaxBehavior,
|
tax_behavior: stripeTaxBehavior,
|
||||||
});
|
});
|
||||||
const result = transformStripePriceEventToPriceRepositoryData(
|
const result = transformStripePriceEventToDatabasePrice(mockData as any);
|
||||||
mockData as any,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.taxBehavior).toBe(expectedTaxBehavior);
|
expect(result.taxBehavior).toBe(expectedTaxBehavior);
|
||||||
});
|
});
|
||||||
@ -90,9 +86,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
|
|||||||
|
|
||||||
priceTypes.forEach(([stripeType, expectedType]) => {
|
priceTypes.forEach(([stripeType, expectedType]) => {
|
||||||
const mockData = createMockPriceData({ type: stripeType });
|
const mockData = createMockPriceData({ type: stripeType });
|
||||||
const result = transformStripePriceEventToPriceRepositoryData(
|
const result = transformStripePriceEventToDatabasePrice(mockData as any);
|
||||||
mockData as any,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.type).toBe(expectedType);
|
expect(result.type).toBe(expectedType);
|
||||||
});
|
});
|
||||||
@ -106,9 +100,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
|
|||||||
|
|
||||||
billingSchemes.forEach(([stripeScheme, expectedScheme]) => {
|
billingSchemes.forEach(([stripeScheme, expectedScheme]) => {
|
||||||
const mockData = createMockPriceData({ billing_scheme: stripeScheme });
|
const mockData = createMockPriceData({ billing_scheme: stripeScheme });
|
||||||
const result = transformStripePriceEventToPriceRepositoryData(
|
const result = transformStripePriceEventToDatabasePrice(mockData as any);
|
||||||
mockData as any,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.billingScheme).toBe(expectedScheme);
|
expect(result.billingScheme).toBe(expectedScheme);
|
||||||
});
|
});
|
||||||
@ -124,9 +116,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
|
|||||||
const mockData = createMockPriceData({
|
const mockData = createMockPriceData({
|
||||||
recurring: { usage_type: stripeUsageType, interval: 'month' },
|
recurring: { usage_type: stripeUsageType, interval: 'month' },
|
||||||
});
|
});
|
||||||
const result = transformStripePriceEventToPriceRepositoryData(
|
const result = transformStripePriceEventToDatabasePrice(mockData as any);
|
||||||
mockData as any,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.usageType).toBe(expectedUsageType);
|
expect(result.usageType).toBe(expectedUsageType);
|
||||||
});
|
});
|
||||||
@ -140,9 +130,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
|
|||||||
|
|
||||||
tiersModes.forEach(([stripeTiersMode, expectedTiersMode]) => {
|
tiersModes.forEach(([stripeTiersMode, expectedTiersMode]) => {
|
||||||
const mockData = createMockPriceData({ tiers_mode: stripeTiersMode });
|
const mockData = createMockPriceData({ tiers_mode: stripeTiersMode });
|
||||||
const result = transformStripePriceEventToPriceRepositoryData(
|
const result = transformStripePriceEventToDatabasePrice(mockData as any);
|
||||||
mockData as any,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.tiersMode).toBe(expectedTiersMode);
|
expect(result.tiersMode).toBe(expectedTiersMode);
|
||||||
});
|
});
|
||||||
@ -160,9 +148,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
|
|||||||
const mockData = createMockPriceData({
|
const mockData = createMockPriceData({
|
||||||
recurring: { usage_type: 'licensed', interval: stripeInterval },
|
recurring: { usage_type: 'licensed', interval: stripeInterval },
|
||||||
});
|
});
|
||||||
const result = transformStripePriceEventToPriceRepositoryData(
|
const result = transformStripePriceEventToDatabasePrice(mockData as any);
|
||||||
mockData as any,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.interval).toBe(expectedInterval);
|
expect(result.interval).toBe(expectedInterval);
|
||||||
});
|
});
|
||||||
@ -180,9 +166,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
|
|||||||
tiers_mode: 'graduated',
|
tiers_mode: 'graduated',
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = transformStripePriceEventToPriceRepositoryData(
|
const result = transformStripePriceEventToDatabasePrice(mockData as any);
|
||||||
mockData as any,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.billingScheme).toBe(BillingPriceBillingScheme.TIERED);
|
expect(result.billingScheme).toBe(BillingPriceBillingScheme.TIERED);
|
||||||
expect(result.tiers).toEqual(mockTiers);
|
expect(result.tiers).toEqual(mockTiers);
|
||||||
@ -204,9 +188,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
|
|||||||
transform_quantity: mockTransformQuantity,
|
transform_quantity: mockTransformQuantity,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = transformStripePriceEventToPriceRepositoryData(
|
const result = transformStripePriceEventToDatabasePrice(mockData as any);
|
||||||
mockData as any,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.stripeMeterId).toBe('meter_123');
|
expect(result.stripeMeterId).toBe('meter_123');
|
||||||
expect(result.usageType).toBe(BillingUsageType.METERED);
|
expect(result.usageType).toBe(BillingUsageType.METERED);
|
||||||
@ -225,9 +207,7 @@ describe('transformStripePriceEventToPriceRepositoryData', () => {
|
|||||||
currency_options: mockCurrencyOptions,
|
currency_options: mockCurrencyOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = transformStripePriceEventToPriceRepositoryData(
|
const result = transformStripePriceEventToDatabasePrice(mockData as any);
|
||||||
mockData as any,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.currencyOptions).toEqual(mockCurrencyOptions);
|
expect(result.currencyOptions).toEqual(mockCurrencyOptions);
|
||||||
});
|
});
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
import { transformStripeProductEventToProductRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-product-event-to-product-repository-data.util';
|
import { transformStripeProductEventToDatabaseProduct } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-product-event-to-database-product.util';
|
||||||
|
|
||||||
describe('transformStripeProductEventToProductRepositoryData', () => {
|
describe('transformStripeProductEventToDatabaseProduct', () => {
|
||||||
it('should return the correct data', () => {
|
it('should return the correct data', () => {
|
||||||
const data: Stripe.ProductCreatedEvent.Data = {
|
const data: Stripe.ProductCreatedEvent.Data = {
|
||||||
object: {
|
object: {
|
||||||
@ -31,7 +31,7 @@ describe('transformStripeProductEventToProductRepositoryData', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = transformStripeProductEventToProductRepositoryData(data);
|
const result = transformStripeProductEventToDatabaseProduct(data);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
stripeProductId: 'prod_123',
|
stripeProductId: 'prod_123',
|
||||||
@ -71,7 +71,7 @@ describe('transformStripeProductEventToProductRepositoryData', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = transformStripeProductEventToProductRepositoryData(data);
|
const result = transformStripeProductEventToDatabaseProduct(data);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
stripeProductId: 'prod_456',
|
stripeProductId: 'prod_456',
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import { transformStripeSubscriptionEventToCustomerRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-customer-repository-data.util';
|
import { transformStripeSubscriptionEventToDatabaseCustomer } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-customer.util';
|
||||||
|
describe('transformStripeSubscriptionEventToDatabaseCustomer', () => {
|
||||||
describe('transformStripeSubscriptionEventToCustomerRepositoryData', () => {
|
|
||||||
const mockWorkspaceId = 'workspace_123';
|
const mockWorkspaceId = 'workspace_123';
|
||||||
const mockTimestamp = 1672531200; // 2023-01-01 00:00:00 UTC
|
const mockTimestamp = 1672531200; // 2023-01-01 00:00:00 UTC
|
||||||
|
|
||||||
@ -38,7 +37,7 @@ describe('transformStripeSubscriptionEventToCustomerRepositoryData', () => {
|
|||||||
it('should transform basic customer data correctly', () => {
|
it('should transform basic customer data correctly', () => {
|
||||||
const mockData = createMockSubscriptionData('cus_123');
|
const mockData = createMockSubscriptionData('cus_123');
|
||||||
|
|
||||||
const result = transformStripeSubscriptionEventToCustomerRepositoryData(
|
const result = transformStripeSubscriptionEventToDatabaseCustomer(
|
||||||
mockWorkspaceId,
|
mockWorkspaceId,
|
||||||
mockData as any,
|
mockData as any,
|
||||||
);
|
);
|
||||||
@ -54,7 +53,7 @@ describe('transformStripeSubscriptionEventToCustomerRepositoryData', () => {
|
|||||||
|
|
||||||
// Test with different event types (they should all transform the same way)
|
// Test with different event types (they should all transform the same way)
|
||||||
['updated', 'created', 'deleted'].forEach(() => {
|
['updated', 'created', 'deleted'].forEach(() => {
|
||||||
const result = transformStripeSubscriptionEventToCustomerRepositoryData(
|
const result = transformStripeSubscriptionEventToDatabaseCustomer(
|
||||||
mockWorkspaceId,
|
mockWorkspaceId,
|
||||||
mockData as any,
|
mockData as any,
|
||||||
);
|
);
|
||||||
@ -71,7 +70,7 @@ describe('transformStripeSubscriptionEventToCustomerRepositoryData', () => {
|
|||||||
const testWorkspaces = ['workspace_1', 'workspace_2', 'workspace_abc'];
|
const testWorkspaces = ['workspace_1', 'workspace_2', 'workspace_abc'];
|
||||||
|
|
||||||
testWorkspaces.forEach((testWorkspaceId) => {
|
testWorkspaces.forEach((testWorkspaceId) => {
|
||||||
const result = transformStripeSubscriptionEventToCustomerRepositoryData(
|
const result = transformStripeSubscriptionEventToDatabaseCustomer(
|
||||||
testWorkspaceId,
|
testWorkspaceId,
|
||||||
mockData as any,
|
mockData as any,
|
||||||
);
|
);
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { BillingSubscriptionCollectionMethod } from 'src/engine/core-modules/billing/enums/billing-subscription-collection-method.enum';
|
import { BillingSubscriptionCollectionMethod } from 'src/engine/core-modules/billing/enums/billing-subscription-collection-method.enum';
|
||||||
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
|
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
|
||||||
import { transformStripeSubscriptionEventToSubscriptionRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-repository-data.util';
|
import { transformStripeSubscriptionEventToDatabaseSubscription } from 'src/engine/core-modules/billing/webhooks/utils/transform-stripe-subscription-event-to-database-subscription.util';
|
||||||
|
|
||||||
describe('transformStripeSubscriptionEventToSubscriptionRepositoryData', () => {
|
describe('transformStripeSubscriptionEventToDatabaseSubscription', () => {
|
||||||
const mockWorkspaceId = 'workspace-123';
|
const mockWorkspaceId = 'workspace-123';
|
||||||
const mockTimestamp = 1672531200; // 2023-01-01 00:00:00 UTC
|
const mockTimestamp = 1672531200; // 2023-01-01 00:00:00 UTC
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ describe('transformStripeSubscriptionEventToSubscriptionRepositoryData', () => {
|
|||||||
|
|
||||||
it('should transform basic subscription data correctly', () => {
|
it('should transform basic subscription data correctly', () => {
|
||||||
const mockData = createMockSubscriptionData();
|
const mockData = createMockSubscriptionData();
|
||||||
const result = transformStripeSubscriptionEventToSubscriptionRepositoryData(
|
const result = transformStripeSubscriptionEventToDatabaseSubscription(
|
||||||
mockWorkspaceId,
|
mockWorkspaceId,
|
||||||
mockData as any,
|
mockData as any,
|
||||||
);
|
);
|
||||||
@ -83,11 +83,10 @@ describe('transformStripeSubscriptionEventToSubscriptionRepositoryData', () => {
|
|||||||
const mockData = createMockSubscriptionData({
|
const mockData = createMockSubscriptionData({
|
||||||
status: stripeStatus,
|
status: stripeStatus,
|
||||||
});
|
});
|
||||||
const result =
|
const result = transformStripeSubscriptionEventToDatabaseSubscription(
|
||||||
transformStripeSubscriptionEventToSubscriptionRepositoryData(
|
mockWorkspaceId,
|
||||||
mockWorkspaceId,
|
mockData as any,
|
||||||
mockData as any,
|
);
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.status).toBe(expectedStatus);
|
expect(result.status).toBe(expectedStatus);
|
||||||
});
|
});
|
||||||
@ -102,7 +101,7 @@ describe('transformStripeSubscriptionEventToSubscriptionRepositoryData', () => {
|
|||||||
trial_end: trialEnd,
|
trial_end: trialEnd,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = transformStripeSubscriptionEventToSubscriptionRepositoryData(
|
const result = transformStripeSubscriptionEventToDatabaseSubscription(
|
||||||
mockWorkspaceId,
|
mockWorkspaceId,
|
||||||
mockData as any,
|
mockData as any,
|
||||||
);
|
);
|
||||||
@ -125,7 +124,7 @@ describe('transformStripeSubscriptionEventToSubscriptionRepositoryData', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = transformStripeSubscriptionEventToSubscriptionRepositoryData(
|
const result = transformStripeSubscriptionEventToDatabaseSubscription(
|
||||||
mockWorkspaceId,
|
mockWorkspaceId,
|
||||||
mockData as any,
|
mockData as any,
|
||||||
);
|
);
|
||||||
@ -148,7 +147,7 @@ describe('transformStripeSubscriptionEventToSubscriptionRepositoryData', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = transformStripeSubscriptionEventToSubscriptionRepositoryData(
|
const result = transformStripeSubscriptionEventToDatabaseSubscription(
|
||||||
mockWorkspaceId,
|
mockWorkspaceId,
|
||||||
mockData as any,
|
mockData as any,
|
||||||
);
|
);
|
||||||
@ -172,11 +171,10 @@ describe('transformStripeSubscriptionEventToSubscriptionRepositoryData', () => {
|
|||||||
const mockData = createMockSubscriptionData({
|
const mockData = createMockSubscriptionData({
|
||||||
collection_method: stripeMethod,
|
collection_method: stripeMethod,
|
||||||
});
|
});
|
||||||
const result =
|
const result = transformStripeSubscriptionEventToDatabaseSubscription(
|
||||||
transformStripeSubscriptionEventToSubscriptionRepositoryData(
|
mockWorkspaceId,
|
||||||
mockWorkspaceId,
|
mockData as any,
|
||||||
mockData as any,
|
);
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.collectionMethod).toBe(expectedMethod);
|
expect(result.collectionMethod).toBe(expectedMethod);
|
||||||
});
|
});
|
||||||
@ -187,7 +185,7 @@ describe('transformStripeSubscriptionEventToSubscriptionRepositoryData', () => {
|
|||||||
currency: 'eur',
|
currency: 'eur',
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = transformStripeSubscriptionEventToSubscriptionRepositoryData(
|
const result = transformStripeSubscriptionEventToDatabaseSubscription(
|
||||||
mockWorkspaceId,
|
mockWorkspaceId,
|
||||||
mockData as any,
|
mockData as any,
|
||||||
);
|
);
|
||||||
@ -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 { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum';
|
||||||
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum';
|
||||||
|
|
||||||
export const transformStripePriceEventToPriceRepositoryData = (
|
export const transformStripePriceEventToDatabasePrice = (
|
||||||
data: Stripe.PriceCreatedEvent.Data | Stripe.PriceUpdatedEvent.Data,
|
data: Stripe.PriceCreatedEvent.Data | Stripe.PriceUpdatedEvent.Data,
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
@ -1,13 +1,13 @@
|
|||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
export const transformStripeProductEventToProductRepositoryData = (
|
export const transformStripeProductEventToDatabaseProduct = (
|
||||||
data: Stripe.ProductUpdatedEvent.Data | Stripe.ProductCreatedEvent.Data,
|
data: Stripe.ProductUpdatedEvent.Data | Stripe.ProductCreatedEvent.Data,
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
stripeProductId: data.object.id,
|
stripeProductId: data.object.id,
|
||||||
name: data.object.name,
|
name: data.object.name,
|
||||||
active: data.object.active,
|
active: data.object.active,
|
||||||
description: data.object.description,
|
description: data.object.description ?? '',
|
||||||
images: data.object.images,
|
images: data.object.images,
|
||||||
marketingFeatures: data.object.marketing_features,
|
marketingFeatures: data.object.marketing_features,
|
||||||
defaultStripePriceId: data.object.default_price
|
defaultStripePriceId: data.object.default_price
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
export const transformStripeSubscriptionEventToCustomerRepositoryData = (
|
export const transformStripeSubscriptionEventToDatabaseCustomer = (
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
data:
|
data:
|
||||||
| Stripe.CustomerSubscriptionUpdatedEvent.Data
|
| Stripe.CustomerSubscriptionUpdatedEvent.Data
|
||||||
@ -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 { BillingSubscriptionCollectionMethod } from 'src/engine/core-modules/billing/enums/billing-subscription-collection-method.enum';
|
||||||
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
|
import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum';
|
||||||
|
|
||||||
export const transformStripeSubscriptionEventToSubscriptionRepositoryData = (
|
export const transformStripeSubscriptionEventToDatabaseSubscription = (
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
data:
|
data:
|
||||||
| Stripe.CustomerSubscriptionUpdatedEvent.Data
|
| Stripe.CustomerSubscriptionUpdatedEvent.Data
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||||
|
|
||||||
import { TrialPeriodDTO } from 'src/engine/core-modules/billing/dto/trial-period.dto';
|
import { BillingTrialPeriodDTO } from 'src/engine/core-modules/billing/dtos/billing-trial-period.dto';
|
||||||
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
|
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
|
||||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||||
import { AuthProviders } from 'src/engine/core-modules/workspace/dtos/public-workspace-data-output';
|
import { AuthProviders } from 'src/engine/core-modules/workspace/dtos/public-workspace-data-output';
|
||||||
@ -17,8 +17,8 @@ class Billing {
|
|||||||
@Field(() => String, { nullable: true })
|
@Field(() => String, { nullable: true })
|
||||||
billingUrl?: string;
|
billingUrl?: string;
|
||||||
|
|
||||||
@Field(() => [TrialPeriodDTO])
|
@Field(() => [BillingTrialPeriodDTO])
|
||||||
trialPeriods: TrialPeriodDTO[];
|
trialPeriods: BillingTrialPeriodDTO[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
|
|||||||
@ -15,4 +15,5 @@ export enum FeatureFlagKey {
|
|||||||
IsCommandMenuV2Enabled = 'IS_COMMAND_MENU_V2_ENABLED',
|
IsCommandMenuV2Enabled = 'IS_COMMAND_MENU_V2_ENABLED',
|
||||||
IsJsonFilterEnabled = 'IS_JSON_FILTER_ENABLED',
|
IsJsonFilterEnabled = 'IS_JSON_FILTER_ENABLED',
|
||||||
IsLocalizationEnabled = 'IS_LOCALIZATION_ENABLED',
|
IsLocalizationEnabled = 'IS_LOCALIZATION_ENABLED',
|
||||||
|
IsBillingPlansEnabled = 'IS_BILLING_PLANS_ENABLED',
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user