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,