From f2d0a1b90a513a998bd5fd784a2497c4d1366366 Mon Sep 17 00:00:00 2001 From: Etienne <45695613+etiennejouan@users.noreply.github.com> Date: Thu, 9 Jul 2026 09:38:30 +0200 Subject: [PATCH] fix(billing): only reactivate suspended workspaces when subscription is in good standing (#22687) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Context When a subscription's trial ends without a valid payment method, Stripe emits `customer.subscription.updated` (`active → past_due`) within seconds of `trial_end`. Our webhook handler correctly suspends the workspace for this event, because it lands inside the 24h "trial just ended" window checked by `shouldSuspendWorkspace`. However, the workspace does not *stay* suspended if an other subscription update event arrived. ## Problem Reactivation was gated only on the negation of the suspend heuristic: ```ts } else if (workspace.activationStatus === WorkspaceActivationStatus.SUSPENDED) { await this.workspaceService.reactivateWorkspace(workspaceId); } Review in cubic --- .../billing-webhook-subscription.service.ts | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/twenty-server/src/engine/core-modules/billing-webhook/services/billing-webhook-subscription.service.ts b/packages/twenty-server/src/engine/core-modules/billing-webhook/services/billing-webhook-subscription.service.ts index 69c0180686..d520dc962e 100644 --- a/packages/twenty-server/src/engine/core-modules/billing-webhook/services/billing-webhook-subscription.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing-webhook/services/billing-webhook-subscription.service.ts @@ -150,9 +150,7 @@ export class BillingWebhookSubscriptionService { 'currentBillingSubscription', ]); - const shouldSuspend = this.shouldSuspendWorkspace(data); - - if (shouldSuspend) { + if (this.shouldSuspendWorkspace(data)) { if (workspace.activationStatus === WorkspaceActivationStatus.ACTIVE) { await this.workspaceService.suspendWorkspace(workspaceId); } else if ( @@ -162,6 +160,7 @@ export class BillingWebhookSubscriptionService { await this.workspaceService.deleteWorkspace(workspace.id); } } else if ( + this.shouldReactivateWorkspace(data) && workspace.activationStatus === WorkspaceActivationStatus.SUSPENDED ) { await this.workspaceService.reactivateWorkspace(workspaceId); @@ -216,6 +215,22 @@ export class BillingWebhookSubscriptionService { ); } + shouldReactivateWorkspace( + data: + | Stripe.CustomerSubscriptionUpdatedEvent.Data + | Stripe.CustomerSubscriptionCreatedEvent.Data + | Stripe.CustomerSubscriptionDeletedEvent.Data, + ): boolean { + const status = data.object.status as SubscriptionStatus; + + const activeStatuses = [ + SubscriptionStatus.Active, + SubscriptionStatus.Trialing, + ]; + + return activeStatuses.includes(status); + } + async updateBillingSubscriptionItems( subscriptionId: string, event: