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 0000000000..668fd4a8f4 --- /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 6ca6e73fb6..b5113fe6ce 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 10e34c755e..59ef950274 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 fabffc03f0..39e387c037 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 3b0b8be801..354479d68d 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 84ed85260b..c36988d651 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 785759d104..02b194501f 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 d2ed20e5bc..88e4caffc9 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 c821dc49c1..d275746add 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 0000000000..8e87fc0959 --- /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 4a7ed22bb2..1c9fcf8590 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 3d773e47ce..9cd4c62551 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 efd9c02acb..85e75f7052 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 0000000000..e7ae842305 --- /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 0000000000..5ce42e1e5b --- /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 6d4cf80c28..2fea8d36c6 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 9817937c33..9c72971dce 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 3dacc09dc8..53e37d9a1d 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,