update price on subscription - command (#11698)

This commit is contained in:
Etienne
2025-04-24 09:32:03 +02:00
committed by GitHub
parent 0b729cb000
commit ccc6d968aa
3 changed files with 131 additions and 0 deletions

View File

@ -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,

View File

@ -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<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectRepository(BillingSubscription, 'core')
protected readonly billingSubscriptionRepository: Repository<BillingSubscription>,
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<void> {
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}`,
);
}
}

View File

@ -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,
});
}
}