-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add price and meter dynamically add foreign keys in billing (#9100)
**TLDR** Solves: twentyhq/private-issues#199 Partially solves: twentyhq/private-issues#221 (more details below) Updates the BillingMeter and BillingPrice tables while listening to the events "price.created" and "price.updated" from the stripe webhook. Also added the foreign keys, that couldn't be added to the BillingEntities. **In Order To test** Billing: - Set IS_BILLING_ENABLED to true - Add your BILLING_STRIPE_SECRET and BILLING_STRIPE_API_KEY - Add your BILLING_STRIPE_BASE_PLAN_PRODUCT_ID (use the one in testMode > Base Plan) Authenticate with your account in the stripe CLI Run the command: stripe listen --forward-to http://localhost:3000/billing/webhooks Run the twenty workker Authenticate yourself on the app choose a plan and run the app normally. In stripe and in posgress the customer table data should be added. **Take Into Consideration** In a previous migration the foreign key to workpaceId was taken down this was due to the separation of the migrations if billing is enabled. Because we want to separate in these two categories: we will be polluting the Common Migrations with relations to tables that don't exists. This will be addressed in a PR in the next sprint (perhaps a decorator?) **Doing** Testing migrations, when we are in main and when billing is enabled.
- Loading branch information
Showing
18 changed files
with
348 additions
and
30 deletions.
There are no files selected for viewing
67 changes: 67 additions & 0 deletions
67
...c/database/typeorm/core/migrations/billing/1734450749954-addConstraintsOnBillingTables.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import { MigrationInterface, QueryRunner } from 'typeorm'; | ||
|
||
export class AddConstraintsOnBillingTables1734450749954 | ||
implements MigrationInterface | ||
{ | ||
name = 'AddConstraintsOnBillingTables1734450749954'; | ||
|
||
public async up(queryRunner: QueryRunner): Promise<void> { | ||
await queryRunner.query( | ||
`ALTER TABLE "core"."billingSubscriptionItem" DROP CONSTRAINT "IndexOnBillingSubscriptionIdAndStripeSubscriptionItemIdUnique"`, | ||
); | ||
await queryRunner.query( | ||
`ALTER TABLE "core"."billingCustomer" DROP CONSTRAINT "IndexOnWorkspaceIdAndStripeCustomerIdUnique"`, | ||
); | ||
await queryRunner.query( | ||
`ALTER TABLE "core"."billingSubscriptionItem" ADD CONSTRAINT "UQ_6a989264cab5ee2d4b424e78526" UNIQUE ("stripeSubscriptionItemId")`, | ||
); | ||
await queryRunner.query( | ||
`ALTER TABLE "core"."billingSubscriptionItem" DROP COLUMN "quantity"`, | ||
); | ||
await queryRunner.query( | ||
`ALTER TABLE "core"."billingSubscriptionItem" ADD "quantity" numeric`, | ||
); | ||
await queryRunner.query( | ||
`ALTER TABLE "core"."billingCustomer" ADD CONSTRAINT "UQ_53c2ef50e9611082f83d760897d" UNIQUE ("workspaceId")`, | ||
); | ||
await queryRunner.query( | ||
`CREATE UNIQUE INDEX "IndexOnActiveSubscriptionPerWorkspace" ON "core"."billingSubscription" ("workspaceId") WHERE status IN ('trialing', 'active', 'past_due')`, | ||
); | ||
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<void> { | ||
await queryRunner.query( | ||
`ALTER TABLE "core"."billingSubscription" DROP CONSTRAINT "FK_9120b7586c3471463480b58d20a"`, | ||
); | ||
await queryRunner.query( | ||
`ALTER TABLE "core"."billingEntitlement" DROP CONSTRAINT "FK_766a1918aa3dbe0d67d3df62356"`, | ||
); | ||
await queryRunner.query( | ||
`DROP INDEX "core"."IndexOnActiveSubscriptionPerWorkspace"`, | ||
); | ||
await queryRunner.query( | ||
`ALTER TABLE "core"."billingCustomer" DROP CONSTRAINT "UQ_53c2ef50e9611082f83d760897d"`, | ||
); | ||
await queryRunner.query( | ||
`ALTER TABLE "core"."billingSubscriptionItem" DROP COLUMN "quantity"`, | ||
); | ||
await queryRunner.query( | ||
`ALTER TABLE "core"."billingSubscriptionItem" ADD "quantity" integer NOT NULL`, | ||
); | ||
await queryRunner.query( | ||
`ALTER TABLE "core"."billingSubscriptionItem" DROP CONSTRAINT "UQ_6a989264cab5ee2d4b424e78526"`, | ||
); | ||
await queryRunner.query( | ||
`ALTER TABLE "core"."billingCustomer" ADD CONSTRAINT "IndexOnWorkspaceIdAndStripeCustomerIdUnique" UNIQUE ("workspaceId", "stripeCustomerId")`, | ||
); | ||
await queryRunner.query( | ||
`ALTER TABLE "core"."billingSubscriptionItem" ADD CONSTRAINT "IndexOnBillingSubscriptionIdAndStripeSubscriptionItemIdUnique" UNIQUE ("billingSubscriptionId", "stripeSubscriptionItemId")`, | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
67 changes: 67 additions & 0 deletions
67
...s/twenty-server/src/engine/core-modules/billing/services/billing-webhook-price.service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import { Injectable, Logger } from '@nestjs/common'; | ||
import { InjectRepository } from '@nestjs/typeorm'; | ||
|
||
import Stripe from 'stripe'; | ||
import { Repository } from 'typeorm'; | ||
|
||
import { | ||
BillingException, | ||
BillingExceptionCode, | ||
} from 'src/engine/core-modules/billing/billing.exception'; | ||
import { BillingMeter } from 'src/engine/core-modules/billing/entities/billing-meter.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 { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service'; | ||
import { transformStripeMeterDataToMeterRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-meter-data-to-meter-repository-data.util'; | ||
import { transformStripePriceEventToPriceRepositoryData } from 'src/engine/core-modules/billing/utils/transform-stripe-price-event-to-price-repository-data.util'; | ||
@Injectable() | ||
export class BillingWebhookPriceService { | ||
protected readonly logger = new Logger(BillingWebhookPriceService.name); | ||
constructor( | ||
private readonly stripeService: StripeService, | ||
@InjectRepository(BillingPrice, 'core') | ||
private readonly billingPriceRepository: Repository<BillingPrice>, | ||
@InjectRepository(BillingMeter, 'core') | ||
private readonly billingMeterRepository: Repository<BillingMeter>, | ||
@InjectRepository(BillingProduct, 'core') | ||
private readonly billingProductRepository: Repository<BillingProduct>, | ||
) {} | ||
|
||
async processStripeEvent( | ||
data: Stripe.PriceCreatedEvent.Data | Stripe.PriceUpdatedEvent.Data, | ||
) { | ||
const stripeProductId = String(data.object.product); | ||
const product = await this.billingProductRepository.findOne({ | ||
where: { stripeProductId }, | ||
}); | ||
|
||
if (!product) { | ||
throw new BillingException( | ||
'Billing product not found', | ||
BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND, | ||
); | ||
} | ||
|
||
const meterId = data.object.recurring?.meter; | ||
|
||
if (meterId) { | ||
const meterData = await this.stripeService.getMeter(meterId); | ||
|
||
await this.billingMeterRepository.upsert( | ||
transformStripeMeterDataToMeterRepositoryData(meterData), | ||
{ | ||
conflictPaths: ['stripeMeterId'], | ||
skipUpdateIfNoValuesChanged: true, | ||
}, | ||
); | ||
} | ||
|
||
await this.billingPriceRepository.upsert( | ||
transformStripePriceEventToPriceRepositoryData(data), | ||
{ | ||
conflictPaths: ['stripePriceId'], | ||
skipUpdateIfNoValuesChanged: true, | ||
}, | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.