fix(billing): only reactivate suspended workspaces when subscription is in good standing (#22687)

## 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);
}

<!-- This is an auto-generated description by cubic. -->
<a href="https://cubic.dev/pr/twentyhq/twenty/pull/22687?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Etienne
2026-07-09 09:38:30 +02:00
committed by GitHub
parent 9da5383289
commit f2d0a1b90a
@@ -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: