account; $accountId = (string) $workspace->account_id; $deleted = false; $settlement = StrandedSettlement::none(); DB::transaction(function () use ($workspace, $account, &$deleted, &$settlement): void { // Serialize deletes per account so the last-workspace SaaS guard // cannot race with a concurrent delete of the sibling workspace. if ($account?->id) { Account::query()->whereKey($account->id)->lockForUpdate()->first(); } $workspaceCount = Workspace::query() ->where('account_id', $workspace->account_id) ->count(); if (! config('trypost.self_hosted') && $workspaceCount <= 1) { return; } ReassignCurrentWorkspace::awayFromWorkspace( workspace: $workspace, attachOwnerFallback: true, account: $account, ); self::pruneInvitesForWorkspace($workspace); // Capture paths inside the lock so uploads that raced into the // transaction are included in post-commit filesystem cleanup. $mediaPaths = PurgeWorkspace::execute($workspace); $settlement = new StrandedSettlement(mediaPaths: $mediaPaths); if ($account) { $settlement = $settlement->merge( SettleStrandedMember::forAccountMembers( $account, exceptUserId: $account->owner_id, onlyWithoutMemberships: true, ), ); } $deleted = true; }); if (! $deleted) { return false; } $settlement->flush(); $account?->syncWorkspaceQuantity(); if (PostHogService::isEnabled()) { SyncAccountUsage::dispatch($accountId, null); } return true; } private static function pruneInvitesForWorkspace(Workspace $workspace): void { Invite::query() ->where('account_id', $workspace->account_id) ->whereNull('accepted_at') ->whereJsonContains('workspaces', $workspace->id) ->get() ->each(function (Invite $invite) use ($workspace): void { $remaining = collect(data_get($invite, 'workspaces', [])) ->reject(fn (mixed $id): bool => (string) $id === (string) $workspace->id) ->values() ->all(); if ($remaining === []) { $invite->delete(); return; } $invite->update(['workspaces' => $remaining]); }); } }