From 5250d5c8d664e5582cf4fd1c4a8328f168b8d560 Mon Sep 17 00:00:00 2001 From: Etienne <45695613+etiennejouan@users.noreply.github.com> Date: Tue, 22 Apr 2025 14:01:36 +0200 Subject: [PATCH] fix subscription item update (#11648) closes https://twenty-v7.sentry.io/issues/6550388239/?environment=prod&project=4507072499810304&query=is%3Aunresolved%20issue.priority%3A%5Bhigh%2C%20medium%5D&referrer=issue-stream&stream_index=6 --- .../hooks/useGetWorkflowNodeExecutionUsage.ts | 2 +- .../src/pages/settings/SettingsBilling.tsx | 4 +- .../core-modules/billing/billing.exception.ts | 1 + .../filters/billing-api-exception.filter.ts | 1 + .../billing/services/billing-usage.service.ts | 7 + .../billing/services/billing.service.ts | 1 + .../stripe-subscription-updated-events.ts | 658 ++++++++++++++++++ .../billing-webhook-subscription.service.ts | 44 +- ...rom-stripe-subscription-event.util.spec.ts | 33 + ...ids-from-stripe-subscription-event.util.ts | 29 + 10 files changed, 768 insertions(+), 12 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/billing/webhooks/__mocks__/stripe-subscription-updated-events.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/__tests__/get-deleted-stripe-subscription-item-ids-from-stripe-subscription-event.util.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/get-deleted-stripe-subscription-item-ids-from-stripe-subscription-event.util.ts diff --git a/packages/twenty-front/src/modules/billing/hooks/useGetWorkflowNodeExecutionUsage.ts b/packages/twenty-front/src/modules/billing/hooks/useGetWorkflowNodeExecutionUsage.ts index f52a6ebcf..9fe194ec3 100644 --- a/packages/twenty-front/src/modules/billing/hooks/useGetWorkflowNodeExecutionUsage.ts +++ b/packages/twenty-front/src/modules/billing/hooks/useGetWorkflowNodeExecutionUsage.ts @@ -19,7 +19,7 @@ export const useGetWorkflowNodeExecutionUsage = () => { return { usageQuantity: 0, freeUsageQuantity: 0, - includedFreeQuantity: 0, + includedFreeQuantity: 10000, paidUsageQuantity: 0, unitPriceCents: 0, totalCostCents: 0, diff --git a/packages/twenty-front/src/pages/settings/SettingsBilling.tsx b/packages/twenty-front/src/pages/settings/SettingsBilling.tsx index e066cfd21..343450095 100644 --- a/packages/twenty-front/src/pages/settings/SettingsBilling.tsx +++ b/packages/twenty-front/src/pages/settings/SettingsBilling.tsx @@ -106,7 +106,9 @@ export const SettingsBilling = () => { ]} > - + {hasNotCanceledCurrentSubscription && ( + + )}
0) { + await this.billingSubscriptionItemRepository.delete({ + billingSubscriptionId: subscriptionId, + stripeSubscriptionItemId: In(deletedSubscriptionItemIds), + }); + } + + await this.billingSubscriptionItemRepository.upsert( + transformStripeSubscriptionEventToDatabaseSubscriptionItem( + subscriptionId, + event.data, + ), + { + conflictPaths: ['stripeSubscriptionItemId'], + skipUpdateIfNoValuesChanged: true, + }, + ); + } } diff --git a/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/__tests__/get-deleted-stripe-subscription-item-ids-from-stripe-subscription-event.util.spec.ts b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/__tests__/get-deleted-stripe-subscription-item-ids-from-stripe-subscription-event.util.spec.ts new file mode 100644 index 000000000..75c0863bb --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/__tests__/get-deleted-stripe-subscription-item-ids-from-stripe-subscription-event.util.spec.ts @@ -0,0 +1,33 @@ +import { + mockStripeSubscriptionUpdatedEventWithDeletedItem, + mockStripeSubscriptionUpdatedEventWithoutUpdatedItem, + mockStripeSubscriptionUpdatedEventWithUpdatedItemOnly, +} from 'src/engine/core-modules/billing/webhooks/__mocks__/stripe-subscription-updated-events'; +import { getDeletedStripeSubscriptionItemIdsFromStripeSubscriptionEvent } from 'src/engine/core-modules/billing/webhooks/utils/get-deleted-stripe-subscription-item-ids-from-stripe-subscription-event.util'; + +describe('getDeletedStripeSubscriptionItemIdsFromStripeSubscriptionEvent', () => { + it('should return an empty array if subscription items are not updated', () => { + const result = + getDeletedStripeSubscriptionItemIdsFromStripeSubscriptionEvent( + mockStripeSubscriptionUpdatedEventWithoutUpdatedItem, + ); + + expect(result).toEqual([]); + }); + it('should return an empty array if subscription items are updated but not deleted', () => { + const result = + getDeletedStripeSubscriptionItemIdsFromStripeSubscriptionEvent( + mockStripeSubscriptionUpdatedEventWithUpdatedItemOnly, + ); + + expect(result).toEqual([]); + }); + it('should return subscription item ids if subscription items are deleted', () => { + const result = + getDeletedStripeSubscriptionItemIdsFromStripeSubscriptionEvent( + mockStripeSubscriptionUpdatedEventWithDeletedItem, + ); + + expect(result).toEqual(['deleted_item_id']); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/get-deleted-stripe-subscription-item-ids-from-stripe-subscription-event.util.ts b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/get-deleted-stripe-subscription-item-ids-from-stripe-subscription-event.util.ts new file mode 100644 index 000000000..ff16a25f1 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/webhooks/utils/get-deleted-stripe-subscription-item-ids-from-stripe-subscription-event.util.ts @@ -0,0 +1,29 @@ +/* @license Enterprise */ + +import Stripe from 'stripe'; +import { isDefined } from 'twenty-shared/utils'; + +export const getDeletedStripeSubscriptionItemIdsFromStripeSubscriptionEvent = ( + event: + | Stripe.CustomerSubscriptionUpdatedEvent + | Stripe.CustomerSubscriptionCreatedEvent + | Stripe.CustomerSubscriptionDeletedEvent, +): string[] => { + const hasUpdatedSubscriptionItems = isDefined( + event.data.previous_attributes?.items?.data, + ); + + if (!hasUpdatedSubscriptionItems) { + return []; + } + + const subscriptionItemIds = + event.data.object.items.data.map((item) => item.id) ?? []; + + const deletedSubscriptionItemIds = + event.data.previous_attributes?.items?.data + .filter((item) => !subscriptionItemIds.includes(item.id)) + .map((item) => item.id) ?? []; + + return deletedSubscriptionItemIds; +};