mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-23 03:51:36 +03:00
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)?
This commit is contained in:
parent
e84176dc0d
commit
028e5cd940
@ -30,15 +30,9 @@ export class AddConstraintsOnBillingTables1734450749954
|
|||||||
await queryRunner.query(
|
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`,
|
`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<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(
|
|
||||||
`ALTER TABLE "core"."billingSubscription" DROP CONSTRAINT "FK_9120b7586c3471463480b58d20a"`,
|
|
||||||
);
|
|
||||||
await queryRunner.query(
|
await queryRunner.query(
|
||||||
`ALTER TABLE "core"."billingEntitlement" DROP CONSTRAINT "FK_766a1918aa3dbe0d67d3df62356"`,
|
`ALTER TABLE "core"."billingEntitlement" DROP CONSTRAINT "FK_766a1918aa3dbe0d67d3df62356"`,
|
||||||
);
|
);
|
||||||
|
@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||||||
|
|
||||||
import { BillingController } from 'src/engine/core-modules/billing/billing.controller';
|
import { BillingController } from 'src/engine/core-modules/billing/billing.controller';
|
||||||
import { BillingResolver } from 'src/engine/core-modules/billing/billing.resolver';
|
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 { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity';
|
||||||
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 { BillingMeter } from 'src/engine/core-modules/billing/entities/billing-meter.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,
|
BillingWebhookProductService,
|
||||||
BillingWebhookPriceService,
|
BillingWebhookPriceService,
|
||||||
BillingRestApiExceptionFilter,
|
BillingRestApiExceptionFilter,
|
||||||
|
BillingSyncCustomerDataCommand,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
BillingSubscriptionService,
|
BillingSubscriptionService,
|
||||||
|
@ -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<Workspace>,
|
||||||
|
private readonly stripeService: StripeService,
|
||||||
|
@InjectRepository(BillingCustomer, 'core')
|
||||||
|
protected readonly billingCustomerRepository: Repository<BillingCustomer>,
|
||||||
|
) {
|
||||||
|
super(workspaceRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeActiveWorkspacesCommand(
|
||||||
|
_passedParam: string[],
|
||||||
|
options: SyncCustomerDataCommandOptions,
|
||||||
|
workspaceIds: string[],
|
||||||
|
): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -82,6 +82,7 @@ export class BillingSubscription {
|
|||||||
{
|
{
|
||||||
nullable: false,
|
nullable: false,
|
||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
|
createForeignKeyConstraints: false,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@JoinColumn({
|
@JoinColumn({
|
||||||
|
@ -194,4 +194,16 @@ export class StripeService {
|
|||||||
|
|
||||||
return productPrices.sort((a, b) => a.unitAmount - b.unitAmount);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user