From 028e5cd9403b794818b1225100c0f84d8d75dbbf Mon Sep 17 00:00:00 2001 From: Ana Sofia Marin Alexandre <61988046+anamarn@users.noreply.github.com> Date: Thu, 19 Dec 2024 07:30:05 -0300 Subject: [PATCH] add sync customer command and drop subscription customer constraint (#9131) **TLDR:** Solves (https://github.com/twentyhq/private-issues/issues/212) Add command to sync customer data from stripe to BillingCustomerTable for all active workspaces. Drop foreign key contraint on billingCustomer in BillingSubscription (in order to not break the DB). **In order to test:** - Billing should be enabled - Have some workspaces that are active and whose id's are not mentioned in BillingCustomer (but the customer are present in stripe). Run the command: `npx nx run twenty-server:command billing:sync-customer-data` Take into consideration Due that all the previous subscriptions in Stripe have the workspaceId in their metadata, we use that information as source of true for the data sync **Things to do:** - Add tests for Billing utils - Separate StripeService into multipleServices (stripeSubscriptionService, stripePriceService etc) perhaps add them in (https://github.com/twentyhq/private-issues/issues/201)? --- ...450749954-addConstraintsOnBillingTables.ts | 6 -- .../core-modules/billing/billing.module.ts | 2 + .../billing-sync-customer-data.command.ts | 97 +++++++++++++++++++ .../entities/billing-subscription.entity.ts | 1 + .../billing/stripe/stripe.service.ts | 12 +++ 5 files changed, 112 insertions(+), 6 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/billing/commands/billing-sync-customer-data.command.ts diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/billing/1734450749954-addConstraintsOnBillingTables.ts b/packages/twenty-server/src/database/typeorm/core/migrations/billing/1734450749954-addConstraintsOnBillingTables.ts index 668fd4a8f..4371cae8c 100644 --- a/packages/twenty-server/src/database/typeorm/core/migrations/billing/1734450749954-addConstraintsOnBillingTables.ts +++ b/packages/twenty-server/src/database/typeorm/core/migrations/billing/1734450749954-addConstraintsOnBillingTables.ts @@ -30,15 +30,9 @@ export class AddConstraintsOnBillingTables1734450749954 await queryRunner.query( `ALTER TABLE "core"."billingEntitlement" ADD CONSTRAINT "FK_766a1918aa3dbe0d67d3df62356" FOREIGN KEY ("stripeCustomerId") REFERENCES "core"."billingCustomer"("stripeCustomerId") ON DELETE CASCADE ON UPDATE NO ACTION`, ); - await queryRunner.query( - `ALTER TABLE "core"."billingSubscription" ADD CONSTRAINT "FK_9120b7586c3471463480b58d20a" FOREIGN KEY ("stripeCustomerId") REFERENCES "core"."billingCustomer"("stripeCustomerId") ON DELETE CASCADE ON UPDATE NO ACTION`, - ); } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "core"."billingSubscription" DROP CONSTRAINT "FK_9120b7586c3471463480b58d20a"`, - ); await queryRunner.query( `ALTER TABLE "core"."billingEntitlement" DROP CONSTRAINT "FK_766a1918aa3dbe0d67d3df62356"`, ); 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 39e387c03..16acfa398 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 @@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { BillingController } from 'src/engine/core-modules/billing/billing.controller'; import { BillingResolver } from 'src/engine/core-modules/billing/billing.resolver'; +import { BillingSyncCustomerDataCommand } from 'src/engine/core-modules/billing/commands/billing-sync-customer-data.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'; @@ -59,6 +60,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; BillingWebhookProductService, BillingWebhookPriceService, BillingRestApiExceptionFilter, + BillingSyncCustomerDataCommand, ], exports: [ BillingSubscriptionService, diff --git a/packages/twenty-server/src/engine/core-modules/billing/commands/billing-sync-customer-data.command.ts b/packages/twenty-server/src/engine/core-modules/billing/commands/billing-sync-customer-data.command.ts new file mode 100644 index 000000000..70fcb981a --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/commands/billing-sync-customer-data.command.ts @@ -0,0 +1,97 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import chalk from 'chalk'; +import { Command } from 'nest-commander'; +import { Repository } from 'typeorm'; + +import { + ActiveWorkspacesCommandOptions, + ActiveWorkspacesCommandRunner, +} from 'src/database/commands/active-workspaces.command'; +import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity'; +import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; + +interface SyncCustomerDataCommandOptions + extends ActiveWorkspacesCommandOptions {} + +@Command({ + name: 'billing:sync-customer-data', + description: 'Sync customer data from Stripe for all active workspaces', +}) +export class BillingSyncCustomerDataCommand extends ActiveWorkspacesCommandRunner { + constructor( + @InjectRepository(Workspace, 'core') + protected readonly workspaceRepository: Repository, + private readonly stripeService: StripeService, + @InjectRepository(BillingCustomer, 'core') + protected readonly billingCustomerRepository: Repository, + ) { + super(workspaceRepository); + } + + async executeActiveWorkspacesCommand( + _passedParam: string[], + options: SyncCustomerDataCommandOptions, + workspaceIds: string[], + ): Promise { + this.logger.log('Running command to sync customer data'); + + for (const workspaceId of workspaceIds) { + this.logger.log(`Running command for workspace ${workspaceId}`); + + try { + await this.syncCustomerDataForWorkspace(workspaceId, options); + } catch (error) { + this.logger.log( + chalk.red( + `Running command on workspace ${workspaceId} failed with error: ${error}, ${error.stack}`, + ), + ); + continue; + } finally { + this.logger.log( + chalk.green(`Finished running command for workspace ${workspaceId}.`), + ); + } + } + + this.logger.log(chalk.green(`Command completed!`)); + } + + private async syncCustomerDataForWorkspace( + workspaceId: string, + options: SyncCustomerDataCommandOptions, + ): Promise { + const billingCustomer = await this.billingCustomerRepository.findOne({ + where: { + workspaceId, + }, + }); + + if (!options.dryRun && !billingCustomer) { + const stripeCustomerId = + await this.stripeService.getStripeCustomerIdFromWorkspaceId( + workspaceId, + ); + + if (stripeCustomerId) { + await this.billingCustomerRepository.upsert( + { + stripeCustomerId, + workspaceId, + }, + { + conflictPaths: ['workspaceId'], + }, + ); + } + } + + if (options.verbose) { + this.logger.log( + chalk.yellow(`Added ${workspaceId} to billingCustomer table`), + ); + } + } +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription.entity.ts b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription.entity.ts index 88e4caffc..419c72990 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription.entity.ts @@ -82,6 +82,7 @@ export class BillingSubscription { { nullable: false, onDelete: 'CASCADE', + createForeignKeyConstraints: false, }, ) @JoinColumn({ diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts index 85e75f705..06944358c 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts @@ -194,4 +194,16 @@ export class StripeService { return productPrices.sort((a, b) => a.unitAmount - b.unitAmount); } + + async getStripeCustomerIdFromWorkspaceId(workspaceId: string) { + const subscription = await this.stripe.subscriptions.search({ + query: `metadata['workspaceId']:'${workspaceId}'`, + limit: 1, + }); + const stripeCustomerId = subscription.data[0].customer + ? String(subscription.data[0].customer) + : undefined; + + return stripeCustomerId; + } }