fe10975927
## Context Follow-up to #22943, addressing the cubic review comments left on that PR (handled in a follow-up as agreed in the thread). In `BillingWebhookSubscriptionService.processStripeEvent`, the suspend/reactivate decision re-reads the workspace and then acts on it. A concurrent soft-delete landing in that window could transition an already soft-deleted workspace to `SUSPENDED`, contrary to the guarded-transition behavior (cubic P2). ## Fix Added `deletedAt: IsNull()` to the `suspendWorkspace` compare-and-swap WHERE clause, matching the guard already present in `reactivateWorkspace`. A concurrent soft-delete now blocks the suspension instead of transitioning a deleted workspace to `SUSPENDED`. ## On the delete guard (cubic P1) Cubic also flagged that the `PENDING_CREATION` hard-delete path could hard-delete a concurrently soft-deleted workspace. On review this is not worth guarding: - A `PENDING_CREATION` workspace has no DB schema and no records (activation is what creates them), so there is no data to lose. - The cleaner already hard-deletes soft-deleted workspaces by design (`cleaner.workspace-service.ts` soft-deletes a pending workspace, then hard-deletes it on a later run), so "hard delete an already soft-deleted workspace" is a supported transition, not corruption. So P1 is intentionally left out to keep `deleteWorkspace` and all its callers unchanged. ## Tests - `suspendWorkspace` update includes `deletedAt IS NULL` and reports whether the guarded update applied. Typecheck, lint, and format pass on the changed files.