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.
This commit is contained in:
Etienne
2026-01-09 19:11:41 +01:00
committed by GitHub
parent 50eb639304
commit f3d89ca9b9
@@ -70,25 +70,36 @@ export class CleanerWorkspaceService {
);
}
async computeWorkspaceBillingInactivity(
async computeDaysSinceSubscriptionUnpaidOrThrow(
workspace: WorkspaceEntity,
): Promise<number> {
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(