From f3d89ca9b98e87434d8763f1330d86ee5367e131 Mon Sep 17 00:00:00 2001 From: Etienne <45695613+etiennejouan@users.noreply.github.com> Date: Fri, 9 Jan 2026 19:11:41 +0100 Subject: [PATCH] Fix cleaning command (#17040) Previous behavior: Used billingSubscription.updatedAt (when the subscription record was last modified) Problem: updatedAt changes for ANY update, not just status changes, making it unreliable for tracking when payment problems started Current behavior: Uses currentPeriodStart when subscription status is Unpaid or Canceled Why it works: WORKSPACE_INACTIVE_DAYS_BEFORE_SOFT_DELETION is shorter than the minimum billing interval (1 month). Then if a workspace is unpaid for the entire current billing period, it will always exceed the deletion threshold before the next period starts Ideal fix: Track workspace.suspendedAt explicitly, giving you the exact timestamp of when payment problems began, regardless of billing periods. --- .../services/cleaner.workspace-service.ts | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts index 63f3e07723..8844c6e70e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts @@ -70,25 +70,36 @@ export class CleanerWorkspaceService { ); } - async computeWorkspaceBillingInactivity( + async computeDaysSinceSubscriptionUnpaidOrThrow( workspace: WorkspaceEntity, ): Promise { try { const lastSubscription = await this.billingSubscriptionRepository.findOneOrFail({ - where: { workspaceId: workspace.id }, + where: { + workspaceId: workspace.id, + }, order: { updatedAt: 'DESC' }, }); - const daysSinceBillingInactivity = differenceInDays( + if ( + lastSubscription.status !== SubscriptionStatus.Unpaid && + lastSubscription.status !== SubscriptionStatus.Canceled + ) { + throw new Error( + 'No cancelled or unpaid billing subscription found for workspace', + ); + } + + const daysSinceSubscriptionUnpaid = differenceInDays( new Date(), - lastSubscription.updatedAt, + lastSubscription.currentPeriodStart, ); - return daysSinceBillingInactivity; + return daysSinceSubscriptionUnpaid; } catch { throw new WorkspaceCleanerException( - `No billing subscription found for workspace ${workspace.id} ${workspace.displayName}`, + `No cancelled or unpaid billing subscription found for workspace ${workspace.id} ${workspace.displayName}`, WorkspaceCleanerExceptionCode.BILLING_SUBSCRIPTION_NOT_FOUND, ); } @@ -374,7 +385,7 @@ export class CleanerWorkspaceService { } const workspaceInactivity = - await this.computeWorkspaceBillingInactivity(workspace); + await this.computeDaysSinceSubscriptionUnpaidOrThrow(workspace); if (workspaceInactivity > this.inactiveDaysBeforeSoftDelete) { await this.informWorkspaceMembersAndSoftDeleteWorkspace(