From ccc6d968aa801ac2226deff8b0f1aa8e7f9a971a Mon Sep 17 00:00:00 2001 From: Etienne <45695613+etiennejouan@users.noreply.github.com> Date: Thu, 24 Apr 2025 09:32:03 +0200 Subject: [PATCH] update price on subscription - command (#11698) --- .../core-modules/billing/billing.module.ts | 2 + ...lling-update-subscription-price.command.ts | 120 ++++++++++++++++++ .../stripe-subscription-item.service.ts | 9 ++ 3 files changed, 131 insertions(+) create mode 100644 packages/twenty-server/src/engine/core-modules/billing/commands/billing-update-subscription-price.command.ts diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts index 9124de2bc..c4d388033 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts @@ -8,6 +8,7 @@ import { BillingResolver } from 'src/engine/core-modules/billing/billing.resolve import { BillingAddWorkflowSubscriptionItemCommand } from 'src/engine/core-modules/billing/commands/billing-add-workflow-subscription-item.command'; import { BillingSyncCustomerDataCommand } from 'src/engine/core-modules/billing/commands/billing-sync-customer-data.command'; import { BillingSyncPlansDataCommand } from 'src/engine/core-modules/billing/commands/billing-sync-plans-data.command'; +import { BillingUpdateSubscriptionPriceCommand } from 'src/engine/core-modules/billing/commands/billing-update-subscription-price.command'; import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity'; import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity'; import { BillingMeter } from 'src/engine/core-modules/billing/entities/billing-meter.entity'; @@ -84,6 +85,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi BillingWebhookCustomerService, BillingRestApiExceptionFilter, BillingSyncCustomerDataCommand, + BillingUpdateSubscriptionPriceCommand, BillingSyncPlansDataCommand, BillingAddWorkflowSubscriptionItemCommand, BillingUsageService, diff --git a/packages/twenty-server/src/engine/core-modules/billing/commands/billing-update-subscription-price.command.ts b/packages/twenty-server/src/engine/core-modules/billing/commands/billing-update-subscription-price.command.ts new file mode 100644 index 000000000..0a3e848db --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/commands/billing-update-subscription-price.command.ts @@ -0,0 +1,120 @@ +/* @license Enterprise */ + +import { InjectRepository } from '@nestjs/typeorm'; + +import { Command, Option } from 'nest-commander'; +import { isDefined } from 'twenty-shared/utils'; +import { Repository } from 'typeorm'; + +import { + ActiveOrSuspendedWorkspacesMigrationCommandRunner, + RunOnWorkspaceArgs, +} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; +import { + BillingException, + BillingExceptionCode, +} from 'src/engine/core-modules/billing/billing.exception'; +import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; +import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; +import { StripeSubscriptionItemService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; + +@Command({ + name: 'billing:update-subscription-price', + description: 'Update subscription price', +}) +export class BillingUpdateSubscriptionPriceCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner { + private stripePriceIdToUpdate: string; + private newStripePriceId: string; + private clearUsage = false; + + constructor( + @InjectRepository(Workspace, 'core') + protected readonly workspaceRepository: Repository, + protected readonly twentyORMGlobalManager: TwentyORMGlobalManager, + @InjectRepository(BillingSubscription, 'core') + protected readonly billingSubscriptionRepository: Repository, + private readonly billingSubscriptionService: BillingSubscriptionService, + private readonly stripeSubscriptionItemService: StripeSubscriptionItemService, + ) { + super(workspaceRepository, twentyORMGlobalManager); + } + + @Option({ + flags: '--price-to-update-id [stripe_price_id]', + description: 'Stripe price id to update', + required: true, + }) + parseStripePriceIdToMigrate(val: string): string { + this.stripePriceIdToUpdate = val; + + return val; + } + + @Option({ + flags: '--new-price-id [stripe_price_id]', + description: 'New Stripe price id', + required: true, + }) + parseNewStripePriceId(val: string): string { + this.newStripePriceId = val; + + return val; + } + + @Option({ + flags: '--clear-usage', + description: 'Clear usage on subscription item', + required: false, + }) + parseClearUsage() { + this.clearUsage = true; + } + + override async runOnWorkspace({ + workspaceId, + options, + }: RunOnWorkspaceArgs): Promise { + const subscription = + await this.billingSubscriptionService.getCurrentBillingSubscriptionOrThrow( + { workspaceId }, + ); + + if (!isDefined(subscription)) { + throw new BillingException( + `No subscription found for workspace ${workspaceId}`, + BillingExceptionCode.BILLING_SUBSCRIPTION_NOT_FOUND, + ); + } + + const subscriptionItemToUpdate = subscription.billingSubscriptionItems.find( + (item) => item.stripePriceId === this.stripePriceIdToUpdate, + ); + + if (!isDefined(subscriptionItemToUpdate)) { + this.logger.log(`No price to update for workspace ${workspaceId}`); + + return; + } + + if (!options.dryRun) { + await this.stripeSubscriptionItemService.deleteSubscriptionItem( + subscriptionItemToUpdate.stripeSubscriptionItemId, + this.clearUsage, + ); + + await this.stripeSubscriptionItemService.createSubscriptionItem( + subscription.stripeSubscriptionId, + this.newStripePriceId, + isDefined(subscriptionItemToUpdate.quantity) + ? subscriptionItemToUpdate.quantity + : undefined, + ); + } + + this.logger.log( + `Update subscription replacing price ${subscriptionItemToUpdate.stripePriceId} by ${this.newStripePriceId} with clear usage ${this.clearUsage} - workspace ${workspaceId}`, + ); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service.ts index 9da7ec14b..cebdd9c0a 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service.ts @@ -3,6 +3,7 @@ import { Injectable, Logger } from '@nestjs/common'; import Stripe from 'stripe'; +import { isDefined } from 'twenty-shared/utils'; import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; @@ -34,10 +35,18 @@ export class StripeSubscriptionItemService { async createSubscriptionItem( stripeSubscriptionId: string, stripePriceId: string, + quantity?: number, ) { return this.stripe.subscriptionItems.create({ subscription: stripeSubscriptionId, price: stripePriceId, + ...(isDefined(quantity) ? { quantity } : {}), + }); + } + + async deleteSubscriptionItem(stripeItemId: string, clearUsage = false) { + return this.stripe.subscriptionItems.del(stripeItemId, { + clear_usage: clearUsage, }); } }