From 55dc5983a291bae7e166968d3696ac06baccd2eb Mon Sep 17 00:00:00 2001 From: Ana Sofia Marin Alexandre <61988046+anamarn@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:54:56 -0300 Subject: [PATCH] add price and meter dynamically add foreign keys in billing (#9100) **TLDR** Solves: https://github.com/twentyhq/private-issues/issues/199 Partially solves: https://github.com/twentyhq/private-issues/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. --- ...450749954-addConstraintsOnBillingTables.ts | 67 +++++++++++ .../billing/billing.controller.ts | 17 +++ .../core-modules/billing/billing.exception.ts | 1 + .../core-modules/billing/billing.module.ts | 2 + .../entities/billing-customer.entity.ts | 7 +- .../entities/billing-entitlement.entity.ts | 1 - .../billing-subscription-item.entity.ts | 12 +- .../entities/billing-subscription.entity.ts | 9 +- .../enums/billing-webhook-events.enum.ts | 2 + .../services/billing-webhook-price.service.ts | 67 +++++++++++ .../billing-webhook-product.service.ts | 4 +- .../billing-webhook-subscription.service.ts | 2 +- .../billing/stripe/stripe.service.ts | 14 ++- ...eter-data-to-meter-repository-data.util.ts | 40 +++++++ ...ice-event-to-price-repository-data.util.ts | 113 ++++++++++++++++++ ...t-event-to-product-repository-data.util.ts | 5 +- ...-subscription-item-repository-data.util.ts | 5 +- ...nt-to-subscription-repository-data.util.ts | 10 +- 18 files changed, 348 insertions(+), 30 deletions(-) create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/billing/1734450749954-addConstraintsOnBillingTables.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-price.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-meter-data-to-meter-repository-data.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-price-event-to-price-repository-data.util.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 new file mode 100644 index 000000000..668fd4a8f --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/billing/1734450749954-addConstraintsOnBillingTables.ts @@ -0,0 +1,67 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddConstraintsOnBillingTables1734450749954 + implements MigrationInterface +{ + name = 'AddConstraintsOnBillingTables1734450749954'; + + public async up(queryRunner: QueryRunner): Promise { + 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 { + 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")`, + ); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts index 6ca6e73fb..b5113fe6c 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.controller.ts @@ -19,6 +19,7 @@ import { WebhookEvent } from 'src/engine/core-modules/billing/enums/billing-webh import { BillingRestApiExceptionFilter } from 'src/engine/core-modules/billing/filters/billing-api-exception.filter'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing/services/billing-webhook-entitlement.service'; +import { BillingWebhookPriceService } from 'src/engine/core-modules/billing/services/billing-webhook-price.service'; import { BillingWebhookProductService } from 'src/engine/core-modules/billing/services/billing-webhook-product.service'; import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing/services/billing-webhook-subscription.service'; import { StripeService } from 'src/engine/core-modules/billing/stripe/stripe.service'; @@ -33,6 +34,7 @@ export class BillingController { private readonly billingWebhookEntitlementService: BillingWebhookEntitlementService, private readonly billingSubscriptionService: BillingSubscriptionService, private readonly billingWebhookProductService: BillingWebhookProductService, + private readonly billingWebhookPriceService: BillingWebhookPriceService, ) {} @Post('/webhooks') @@ -96,6 +98,21 @@ export class BillingController { ) { await this.billingWebhookProductService.processStripeEvent(event.data); } + if ( + event.type === WebhookEvent.PRICE_CREATED || + event.type === WebhookEvent.PRICE_UPDATED + ) { + try { + await this.billingWebhookPriceService.processStripeEvent(event.data); + } catch (error) { + if ( + error instanceof BillingException && + error.code === BillingExceptionCode.BILLING_PRODUCT_NOT_FOUND + ) { + res.status(404).end(); + } + } + } res.status(200).end(); } diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts index 10e34c755..59ef95027 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.exception.ts @@ -11,4 +11,5 @@ export class BillingException extends CustomException { export enum BillingExceptionCode { BILLING_CUSTOMER_NOT_FOUND = 'BILLING_CUSTOMER_NOT_FOUND', + BILLING_PRODUCT_NOT_FOUND = 'BILLING_PRODUCT_NOT_FOUND', } 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 fabffc03f..39e387c03 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 @@ -15,6 +15,7 @@ import { BillingWorkspaceMemberListener } from 'src/engine/core-modules/billing/ import { BillingPortalWorkspaceService } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billing/services/billing-webhook-entitlement.service'; +import { BillingWebhookPriceService } from 'src/engine/core-modules/billing/services/billing-webhook-price.service'; import { BillingWebhookProductService } from 'src/engine/core-modules/billing/services/billing-webhook-product.service'; import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing/services/billing-webhook-subscription.service'; import { BillingService } from 'src/engine/core-modules/billing/services/billing.service'; @@ -56,6 +57,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; BillingWorkspaceMemberListener, BillingService, BillingWebhookProductService, + BillingWebhookPriceService, BillingRestApiExceptionFilter, ], exports: [ diff --git a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-customer.entity.ts b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-customer.entity.ts index 3b0b8be80..354479d68 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-customer.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-customer.entity.ts @@ -8,7 +8,6 @@ import { OneToMany, PrimaryGeneratedColumn, Relation, - Unique, UpdateDateColumn, } from 'typeorm'; @@ -18,10 +17,6 @@ import { BillingSubscription } from 'src/engine/core-modules/billing/entities/bi @Entity({ name: 'billingCustomer', schema: 'core' }) @ObjectType('billingCustomer') -@Unique('IndexOnWorkspaceIdAndStripeCustomerIdUnique', [ - 'workspaceId', - 'stripeCustomerId', -]) export class BillingCustomer { @IDField(() => UUIDScalarType) @PrimaryGeneratedColumn('uuid') @@ -36,7 +31,7 @@ export class BillingCustomer { @UpdateDateColumn({ type: 'timestamptz' }) updatedAt: Date; - @Column({ nullable: false, type: 'uuid' }) + @Column({ nullable: false, type: 'uuid', unique: true }) workspaceId: string; @Column({ nullable: false, unique: true }) diff --git a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-entitlement.entity.ts b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-entitlement.entity.ts index 84ed85260..c36988d65 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-entitlement.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-entitlement.entity.ts @@ -52,7 +52,6 @@ export class BillingEntitlement { (billingCustomer) => billingCustomer.billingEntitlements, { onDelete: 'CASCADE', - createForeignKeyConstraints: false, // TODO: remove this once the customer table is populated }, ) @JoinColumn({ diff --git a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription-item.entity.ts b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription-item.entity.ts index 785759d10..02b194501 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription-item.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription-item.entity.ts @@ -16,10 +16,6 @@ import { BillingSubscription } from 'src/engine/core-modules/billing/entities/bi 'billingSubscriptionId', 'stripeProductId', ]) -@Unique('IndexOnBillingSubscriptionIdAndStripeSubscriptionItemIdUnique', [ - 'billingSubscriptionId', - 'stripeSubscriptionItemId', -]) export class BillingSubscriptionItem { @PrimaryGeneratedColumn('uuid') id: string; @@ -60,9 +56,9 @@ export class BillingSubscriptionItem { @Column({ nullable: false }) stripePriceId: string; - @Column({ nullable: false }) - stripeSubscriptionItemId: string; //TODO: add unique + @Column({ nullable: false, unique: true }) + stripeSubscriptionItemId: string; - @Column({ nullable: false }) - quantity: number; //TODO: add nullable and modify stripe service + @Column({ nullable: true, type: 'numeric' }) + quantity: number | null; } 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 d2ed20e5b..88e4caffc 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 @@ -6,6 +6,7 @@ import { Column, CreateDateColumn, Entity, + Index, JoinColumn, ManyToOne, OneToMany, @@ -25,6 +26,10 @@ registerEnumType(SubscriptionStatus, { name: 'SubscriptionStatus' }); registerEnumType(SubscriptionInterval, { name: 'SubscriptionInterval' }); @Entity({ name: 'billingSubscription', schema: 'core' }) +@Index('IndexOnActiveSubscriptionPerWorkspace', ['workspaceId'], { + unique: true, + where: `status IN ('trialing', 'active', 'past_due')`, +}) @ObjectType('BillingSubscription') export class BillingSubscription { @IDField(() => UUIDScalarType) @@ -76,14 +81,14 @@ export class BillingSubscription { (billingCustomer) => billingCustomer.billingSubscriptions, { nullable: false, - createForeignKeyConstraints: false, + onDelete: 'CASCADE', }, ) @JoinColumn({ referencedColumnName: 'stripeCustomerId', name: 'stripeCustomerId', }) - billingCustomer: Relation; //let's see if it works + billingCustomer: Relation; @Column({ nullable: false, default: false }) cancelAtPeriodEnd: boolean; diff --git a/packages/twenty-server/src/engine/core-modules/billing/enums/billing-webhook-events.enum.ts b/packages/twenty-server/src/engine/core-modules/billing/enums/billing-webhook-events.enum.ts index c821dc49c..d275746ad 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/enums/billing-webhook-events.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/enums/billing-webhook-events.enum.ts @@ -6,4 +6,6 @@ export enum WebhookEvent { CUSTOMER_ACTIVE_ENTITLEMENT_SUMMARY_UPDATED = 'entitlements.active_entitlement_summary.updated', PRODUCT_CREATED = 'product.created', PRODUCT_UPDATED = 'product.updated', + PRICE_CREATED = 'price.created', + PRICE_UPDATED = 'price.updated', } diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-price.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-price.service.ts new file mode 100644 index 000000000..8e87fc095 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-price.service.ts @@ -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, + @InjectRepository(BillingMeter, 'core') + private readonly billingMeterRepository: Repository, + @InjectRepository(BillingProduct, 'core') + private readonly billingProductRepository: Repository, + ) {} + + 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, + }, + ); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-product.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-product.service.ts index 4a7ed22bb..1c9fcf859 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-product.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-product.service.ts @@ -50,7 +50,7 @@ export class BillingWebhookProductService { return hasBillingPlanKey && hasPriceUsageBased; } - isValidBillingPlanKey(planKey: string | undefined) { + isValidBillingPlanKey(planKey?: string) { switch (planKey) { case BillingPlanKey.BASE_PLAN: return true; @@ -61,7 +61,7 @@ export class BillingWebhookProductService { } } - isValidPriceUsageBased(priceUsageBased: string | undefined) { + isValidPriceUsageBased(priceUsageBased?: string) { switch (priceUsageBased) { case BillingUsageType.METERED: return true; diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-subscription.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-subscription.service.ts index 3d773e47c..9cd4c6255 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-subscription.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook-subscription.service.ts @@ -54,7 +54,7 @@ export class BillingWebhookSubscriptionService { data, ), { - conflictPaths: ['workspaceId', 'stripeCustomerId'], + conflictPaths: ['workspaceId'], skipUpdateIfNoValuesChanged: true, }, ); 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 efd9c02ac..85e75f705 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 @@ -114,10 +114,7 @@ export class StripeService { success_url: successUrl, cancel_url: cancelUrl, }); - } // I prefered to not create a customer with metadat before the checkout, because it would break the tax calculation - // Indeed when the checkout session is created, the customer is created and the tax calculation is done - // If we create a customer before the checkout session, the tax calculation is not done and the checkout session will fail - // I think that it's not risk worth to create a customer before the checkout session, it would only complicate the code for no signigicant gain + } async collectLastInvoice(stripeSubscriptionId: string) { const subscription = await this.stripe.subscriptions.retrieve( @@ -146,7 +143,10 @@ export class StripeService { stripeSubscriptionItem.stripeSubscriptionItemId, { price: stripePriceId, - quantity: stripeSubscriptionItem.quantity, + quantity: + stripeSubscriptionItem.quantity === null + ? undefined + : stripeSubscriptionItem.quantity, }, ); } @@ -164,6 +164,10 @@ export class StripeService { return await this.stripe.customers.retrieve(stripeCustomerId); } + async getMeter(stripeMeterId: string) { + return await this.stripe.billing.meters.retrieve(stripeMeterId); + } + formatProductPrices(prices: Stripe.Price[]): ProductPriceEntity[] { const productPrices: ProductPriceEntity[] = Object.values( prices diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-meter-data-to-meter-repository-data.util.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-meter-data-to-meter-repository-data.util.ts new file mode 100644 index 000000000..e7ae84230 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-meter-data-to-meter-repository-data.util.ts @@ -0,0 +1,40 @@ +import Stripe from 'stripe'; + +import { BillingMeterEventTimeWindow } from 'src/engine/core-modules/billing/enums/billing-meter-event-time-window.enum'; +import { BillingMeterStatus } from 'src/engine/core-modules/billing/enums/billing-meter-status.enum'; + +export const transformStripeMeterDataToMeterRepositoryData = ( + data: Stripe.Billing.Meter, +) => { + return { + stripeMeterId: data.id, + displayName: data.display_name, + eventName: data.event_name, + status: getBillingMeterStatus(data.status), + customerMapping: data.customer_mapping, + eventTimeWindow: data.event_time_window + ? getBillingMeterEventTimeWindow(data.event_time_window) + : undefined, + valueSettings: data.value_settings, + }; +}; + +const getBillingMeterStatus = (data: Stripe.Billing.Meter.Status) => { + switch (data) { + case 'active': + return BillingMeterStatus.ACTIVE; + case 'inactive': + return BillingMeterStatus.INACTIVE; + } +}; + +const getBillingMeterEventTimeWindow = ( + data: Stripe.Billing.Meter.EventTimeWindow, +) => { + switch (data) { + case 'day': + return BillingMeterEventTimeWindow.DAY; + case 'hour': + return BillingMeterEventTimeWindow.HOUR; + } +}; diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-price-event-to-price-repository-data.util.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-price-event-to-price-repository-data.util.ts new file mode 100644 index 000000000..5ce42e1e5 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-price-event-to-price-repository-data.util.ts @@ -0,0 +1,113 @@ +import Stripe from 'stripe'; + +import { BillingPriceBillingScheme } from 'src/engine/core-modules/billing/enums/billing-price-billing-scheme.enum'; +import { BillingPriceTaxBehavior } from 'src/engine/core-modules/billing/enums/billing-price-tax-behavior.enum'; +import { BillingPriceTiersMode } from 'src/engine/core-modules/billing/enums/billing-price-tiers-mode.enum'; +import { BillingPriceType } from 'src/engine/core-modules/billing/enums/billing-price-type.enum'; +import { SubscriptionInterval } from 'src/engine/core-modules/billing/enums/billing-subscription-interval.enum'; +import { BillingUsageType } from 'src/engine/core-modules/billing/enums/billing-usage-type.enum'; + +export const transformStripePriceEventToPriceRepositoryData = ( + data: Stripe.PriceCreatedEvent.Data | Stripe.PriceUpdatedEvent.Data, +) => { + return { + stripePriceId: data.object.id, + active: data.object.active, + stripeProductId: String(data.object.product), + stripeMeterId: data.object.recurring?.meter, + currency: data.object.currency.toUpperCase(), + nickname: data.object.nickname === null ? undefined : data.object.nickname, + taxBehavior: data.object.tax_behavior + ? getTaxBehavior(data.object.tax_behavior) + : undefined, + type: getBillingPriceType(data.object.type), + billingScheme: getBillingPriceBillingScheme(data.object.billing_scheme), + unitAmountDecimal: + data.object.unit_amount_decimal === null + ? undefined + : data.object.unit_amount_decimal, + unitAmount: data.object.unit_amount + ? Number(data.object.unit_amount) + : undefined, + transformQuantity: + data.object.transform_quantity === null + ? undefined + : data.object.transform_quantity, + usageType: data.object.recurring?.usage_type + ? getBillingPriceUsageType(data.object.recurring.usage_type) + : undefined, + interval: data.object.recurring?.interval + ? getBillingPriceInterval(data.object.recurring.interval) + : undefined, + currencyOptions: + data.object.currency_options === null + ? undefined + : data.object.currency_options, + tiers: data.object.tiers === null ? undefined : data.object.tiers, + tiersMode: data.object.tiers_mode + ? getBillingPriceTiersMode(data.object.tiers_mode) + : undefined, + recurring: + data.object.recurring === null ? undefined : data.object.recurring, + }; +}; + +const getTaxBehavior = (data: Stripe.Price.TaxBehavior) => { + switch (data) { + case 'exclusive': + return BillingPriceTaxBehavior.EXCLUSIVE; + case 'inclusive': + return BillingPriceTaxBehavior.INCLUSIVE; + case 'unspecified': + return BillingPriceTaxBehavior.UNSPECIFIED; + } +}; + +const getBillingPriceType = (data: Stripe.Price.Type) => { + switch (data) { + case 'one_time': + return BillingPriceType.ONE_TIME; + case 'recurring': + return BillingPriceType.RECURRING; + } +}; + +const getBillingPriceBillingScheme = (data: Stripe.Price.BillingScheme) => { + switch (data) { + case 'per_unit': + return BillingPriceBillingScheme.PER_UNIT; + case 'tiered': + return BillingPriceBillingScheme.TIERED; + } +}; + +const getBillingPriceUsageType = (data: Stripe.Price.Recurring.UsageType) => { + switch (data) { + case 'licensed': + return BillingUsageType.LICENSED; + case 'metered': + return BillingUsageType.METERED; + } +}; + +const getBillingPriceTiersMode = (data: Stripe.Price.TiersMode) => { + switch (data) { + case 'graduated': + return BillingPriceTiersMode.GRADUATED; + case 'volume': + return BillingPriceTiersMode.VOLUME; + } +}; + +const getBillingPriceInterval = (data: Stripe.Price.Recurring.Interval) => { + switch (data) { + case 'month': + return SubscriptionInterval.Month; + case 'day': + return SubscriptionInterval.Day; + case 'week': + return SubscriptionInterval.Week; + case 'year': + return SubscriptionInterval.Year; + } +}; diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-product-event-to-product-repository-data.util.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-product-event-to-product-repository-data.util.ts index 6d4cf80c2..2fea8d36c 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-product-event-to-product-repository-data.util.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-product-event-to-product-repository-data.util.ts @@ -13,8 +13,9 @@ export const transformStripeProductEventToProductRepositoryData = ( defaultStripePriceId: data.object.default_price ? String(data.object.default_price) : undefined, - unitLabel: data.object.unit_label ?? undefined, - url: data.object.url ?? undefined, + unitLabel: + data.object.unit_label === null ? undefined : data.object.unit_label, + url: data.object.url === null ? undefined : data.object.url, taxCode: data.object.tax_code ? String(data.object.tax_code) : undefined, }; }; diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-item-repository-data.util.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-item-repository-data.util.ts index 9817937c3..9c72971dc 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-item-repository-data.util.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-item-repository-data.util.ts @@ -17,7 +17,10 @@ export const transformStripeSubscriptionEventToSubscriptionItemRepositoryData = stripeSubscriptionItemId: item.id, quantity: item.quantity, metadata: item.metadata, - billingThresholds: item.billing_thresholds ?? undefined, + billingThresholds: + item.billing_thresholds === null + ? undefined + : item.billing_thresholds, }; }); }; diff --git a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-repository-data.util.ts b/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-repository-data.util.ts index 3dacc09dc..53e37d9a1 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-repository-data.util.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/utils/transform-stripe-subscription-event-to-subscription-repository-data.util.ts @@ -25,8 +25,14 @@ export const transformStripeSubscriptionEventToSubscriptionRepositoryData = ( BillingSubscriptionCollectionMethod[ data.object.collection_method.toUpperCase() ], - automaticTax: data.object.automatic_tax ?? undefined, - cancellationDetails: data.object.cancellation_details ?? undefined, + automaticTax: + data.object.automatic_tax === null + ? undefined + : data.object.automatic_tax, + cancellationDetails: + data.object.cancellation_details === null + ? undefined + : data.object.cancellation_details, endedAt: data.object.ended_at ? getDateFromTimestamp(data.object.ended_at) : undefined,