From f243ca9f16e2163993ad58d9be8034f3bae35ca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Birm=C3=A9?= Date: Wed, 11 Nov 2026 14:31:47 +0100 Subject: [PATCH 1/2] feat(cli): enforce token-balance suspension on paid plans below -200 remove-exceeding-tenant-instances previously suspended only FREE-plan tenants. It now also covers self-service paid plans (PERSONAL, PROFESSIONAL, BUSINESS). ENTERPRISE, managed reseller sub-workspaces, tenants with the sub-workspace billing exception, and tenants without a plan record are skipped. Co-Authored-By: Claude Sonnet 5.5 --- packages/cli/src/admin/cmd.test.ts | 46 ++++++++++++++++++++++++ packages/cli/src/admin/cmd.ts | 13 ++++--- packages/cli/src/admin/money.ts | 2 ++ packages/cli/src/admin/suspend-policy.ts | 17 +++++++++ 4 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 packages/cli/src/admin/suspend-policy.ts diff --git a/packages/cli/src/admin/cmd.test.ts b/packages/cli/src/admin/cmd.test.ts index ddc8836..e771b49 100644 --- a/packages/cli/src/admin/cmd.test.ts +++ b/packages/cli/src/admin/cmd.test.ts @@ -97,4 +97,50 @@ describe('remove-exceeding-tenant-instances', () => { expect(summary()).toContain('1 instance failures'); expect(process.exitCode).toBe(1); }); + + it.each(['PERSONAL', 'PROFESSIONAL', 'BUSINESS'])( + 'suspends tenants on the paid plan %s below the threshold', + async (planType) => { + (getTenantPlanMap as jest.Mock).mockResolvedValue({ t1: { planType } }); + mockSuspend.mockResolvedValue(undefined); + await run(); + expect(mockSuspend).toHaveBeenCalledTimes(1); + expect(summary()).toContain('suspended 1 instances'); + } + ); + + it.each([ + ['ENTERPRISE', { planType: 'ENTERPRISE' }], + [ + 'managed sub-workspace', + { planType: 'PROFESSIONAL', managedTenant: true } + ], + [ + 'billing exception', + { planType: 'BUSINESS', subWorkspaceBillingException: true } + ], + ['no plan type', { planType: '' }] + ])('does not suspend %s', async (_name, plan) => { + (getTenantPlanMap as jest.Mock).mockResolvedValue({ t1: plan }); + await run(); + expect(mockSuspend).not.toHaveBeenCalled(); + expect(summary()).toContain('suspended 0 instances'); + }); + + it('does not suspend a paid tenant at or above the threshold', async () => { + (getTenantPlanMap as jest.Mock).mockResolvedValue({ + t1: { planType: 'PERSONAL' } + }); + (getTenantTokenCounts as jest.Mock).mockResolvedValue([ + { tenantId: 't1', remainingTokens: -200 } + ]); + await run(); + expect(mockSuspend).not.toHaveBeenCalled(); + }); + + it('does not suspend a tenant that has no plan record', async () => { + (getTenantPlanMap as jest.Mock).mockResolvedValue({}); + await run(); + expect(mockSuspend).not.toHaveBeenCalled(); + }); }); diff --git a/packages/cli/src/admin/cmd.ts b/packages/cli/src/admin/cmd.ts index 3d64a6b..82670a3 100644 --- a/packages/cli/src/admin/cmd.ts +++ b/packages/cli/src/admin/cmd.ts @@ -31,6 +31,10 @@ import { removeSubscriptionForTenant } from './subscription'; import { getTenantPlanMap, getTenantTokenCounts } from './money'; +import { + isSuspendableByTokenBalance, + TOKEN_SUSPEND_THRESHOLD +} from './suspend-policy'; const ALREADY_SUSPENDED_REASON = 'Instance is already suspended'; @@ -97,7 +101,7 @@ export default function cmdAdmin() { admin .command('remove-exceeding-tenant-instances') .description( - 'Suspend all instances for all tenants with a negative usage token balance' + 'Suspend all instances for tenants below the token balance threshold (-200), on the Free plan and on self-service paid plans' ) .option('--apply', 'Actually suspend instances, otherwise just a dry run') .action(async (options, command) => { @@ -107,7 +111,7 @@ export default function cmdAdmin() { const tenantPlanMap = await getTenantPlanMap(environment); const tokenCounts = await getTenantTokenCounts(environment); const tenantsExceeding = tokenCounts.filter( - (tenant) => tenant.remainingTokens < -200 + (tenant) => tenant.remainingTokens < TOKEN_SUSPEND_THRESHOLD ); let instancesSuspended = 0; let instancesFailed = 0; @@ -115,7 +119,8 @@ export default function cmdAdmin() { let tenantsFailed = 0; const tenantsExceedingCount = tenantsExceeding.length; for (const tenant of tenantsExceeding) { - if (tenantPlanMap[tenant.tenantId]?.planType === 'FREE') { + const plan = tenantPlanMap[tenant.tenantId]; + if (isSuspendableByTokenBalance(plan)) { try { const instancesToRemove = await getInstancesToRemove( tenant.tenantId, @@ -123,7 +128,7 @@ export default function cmdAdmin() { ); if (instancesToRemove.length > 0) { console.log( - `Tenant ${tenant.tenantId} has a negative or low token balance of ${tenant.remainingTokens} tokens and is on the 'FREE' plan` + `Tenant ${tenant.tenantId} has a negative or low token balance of ${tenant.remainingTokens} tokens and is on the '${plan.planType}' plan` ); console.log('Suspending all instances for this tenant...'); for (const item of instancesToRemove) { diff --git a/packages/cli/src/admin/money.ts b/packages/cli/src/admin/money.ts index 3096185..6407410 100644 --- a/packages/cli/src/admin/money.ts +++ b/packages/cli/src/admin/money.ts @@ -11,6 +11,8 @@ export type TenantTokenCount = { export type TenantPlan = { tenantId: string; planType: string; + managedTenant?: boolean; + subWorkspaceBillingException?: boolean; }; export async function getTenantTokenCounts(environment: string) { diff --git a/packages/cli/src/admin/suspend-policy.ts b/packages/cli/src/admin/suspend-policy.ts new file mode 100644 index 0000000..b90f572 --- /dev/null +++ b/packages/cli/src/admin/suspend-policy.ts @@ -0,0 +1,17 @@ +import type { TenantPlan } from './money'; + +export const TOKEN_SUSPEND_THRESHOLD = -200; + +/** + * Whether the token-balance sweep may suspend instances for a tenant on this + * plan. FREE and self-service paid plans are enforced. Tenants without a plan + * record, ENTERPRISE (billed by agreement) and managed reseller sub-workspaces + * or tenants with the sub-workspace billing exception (billed off-platform) + * are never suspended by the sweep. + */ +export function isSuspendableByTokenBalance(plan?: TenantPlan): boolean { + if (!plan || !plan.planType) return false; + if (plan.planType === 'ENTERPRISE') return false; + if (plan.managedTenant || plan.subWorkspaceBillingException) return false; + return true; +} From f35b4a6278616a1c15859ee2512648ded17bc94c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Birm=C3=A9?= Date: Wed, 11 Nov 2026 14:38:12 +0100 Subject: [PATCH 2/2] fix(cli): allowlist plan types and keep suspending flagged FREE tenants Co-Authored-By: Claude Sonnet 5.5 --- packages/cli/src/admin/cmd.test.ts | 16 +++++++++++++++- packages/cli/src/admin/suspend-policy.ts | 20 ++++++++++++-------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/admin/cmd.test.ts b/packages/cli/src/admin/cmd.test.ts index e771b49..a95c408 100644 --- a/packages/cli/src/admin/cmd.test.ts +++ b/packages/cli/src/admin/cmd.test.ts @@ -119,7 +119,8 @@ describe('remove-exceeding-tenant-instances', () => { 'billing exception', { planType: 'BUSINESS', subWorkspaceBillingException: true } ], - ['no plan type', { planType: '' }] + ['no plan type', { planType: '' }], + ['an unknown plan type', { planType: 'FUTURE_PLAN' }] ])('does not suspend %s', async (_name, plan) => { (getTenantPlanMap as jest.Mock).mockResolvedValue({ t1: plan }); await run(); @@ -143,4 +144,17 @@ describe('remove-exceeding-tenant-instances', () => { await run(); expect(mockSuspend).not.toHaveBeenCalled(); }); + + it.each([ + ['managed sub-workspace', { planType: 'FREE', managedTenant: true }], + [ + 'billing exception', + { planType: 'FREE', subWorkspaceBillingException: true } + ] + ])('still suspends a FREE tenant flagged as %s', async (_name, plan) => { + (getTenantPlanMap as jest.Mock).mockResolvedValue({ t1: plan }); + mockSuspend.mockResolvedValue(undefined); + await run(); + expect(mockSuspend).toHaveBeenCalledTimes(1); + }); }); diff --git a/packages/cli/src/admin/suspend-policy.ts b/packages/cli/src/admin/suspend-policy.ts index b90f572..ca28d85 100644 --- a/packages/cli/src/admin/suspend-policy.ts +++ b/packages/cli/src/admin/suspend-policy.ts @@ -2,16 +2,20 @@ import type { TenantPlan } from './money'; export const TOKEN_SUSPEND_THRESHOLD = -200; +const FREE_PLAN = 'FREE'; +const PAID_SELF_SERVICE_PLANS = ['PERSONAL', 'PROFESSIONAL', 'BUSINESS']; + /** * Whether the token-balance sweep may suspend instances for a tenant on this - * plan. FREE and self-service paid plans are enforced. Tenants without a plan - * record, ENTERPRISE (billed by agreement) and managed reseller sub-workspaces - * or tenants with the sub-workspace billing exception (billed off-platform) - * are never suspended by the sweep. + * plan. Allowlist: FREE (always, regardless of flags) and the self-service + * paid plans PERSONAL, PROFESSIONAL and BUSINESS. A paid tenant that is a + * managed reseller sub-workspace, or has the sub-workspace billing exception + * (billed off-platform), is skipped. ENTERPRISE, unknown plan types and + * tenants without a plan record are never suspended by the sweep. */ export function isSuspendableByTokenBalance(plan?: TenantPlan): boolean { - if (!plan || !plan.planType) return false; - if (plan.planType === 'ENTERPRISE') return false; - if (plan.managedTenant || plan.subWorkspaceBillingException) return false; - return true; + if (!plan) return false; + if (plan.planType === FREE_PLAN) return true; + if (!PAID_SELF_SERVICE_PLANS.includes(plan.planType)) return false; + return !plan.managedTenant && !plan.subWorkspaceBillingException; }