Skip to content

Commit 47f375f

Browse files
committed
fix
1 parent f991ca0 commit 47f375f

4 files changed

Lines changed: 89 additions & 3 deletions

File tree

apps/sim/app/workspace/page.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,11 @@ export default function WorkspacePage() {
172172
<WorkspaceStatusCard
173173
title='No workspace access yet'
174174
description={
175-
blockedPolicy.workspaceMode === 'organization'
176-
? "Your account is linked to an organization, but you don't have access to any of its workspaces. Ask an organization admin for workspace access, then check again — or sign out and back in if you recently left the organization."
177-
: 'Your plan has reached its workspace limit and none of your workspaces are active. Upgrade your plan to create another workspace, or contact support to restore an archived one.'
175+
blockedPolicy.blockedReasonCode === 'organization-subscription-inactive'
176+
? "Your organization's subscription is inactive, so new workspaces can't be created. Ask an organization owner to reactivate it."
177+
: blockedPolicy.workspaceMode === 'organization'
178+
? "Your account is linked to an organization, but you don't have access to any of its workspaces. Ask an organization admin for workspace access, then check again — or sign out and back in if you recently left the organization."
179+
: 'Your plan has reached its workspace limit and none of your workspaces are active. Upgrade your plan to create another workspace, or contact support to restore an archived one.'
178180
}
179181
primaryLabel='Check again'
180182
onPrimary={() => window.location.reload()}

apps/sim/lib/api/contracts/workspaces.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ export const workspaceCreationPolicySchema = z.object({
3838
maxWorkspaces: z.number().nullable(),
3939
currentWorkspaceCount: z.number(),
4040
reason: z.string().nullable(),
41+
/**
42+
* Machine-readable discriminant for blocked states whose correct user-facing
43+
* copy the workspace mode alone cannot determine.
44+
*/
45+
blockedReasonCode: z.literal('organization-subscription-inactive').optional(),
4146
})
4247

4348
export type WorkspaceCreationPolicy = z.output<typeof workspaceCreationPolicySchema>

apps/sim/lib/workspaces/policy.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,58 @@ describe('getWorkspaceCreationPolicy', () => {
5959
expect(result.currentWorkspaceCount).toBe(1)
6060
})
6161

62+
it('blocks a plain member of a lapsed organization from creating anything', async () => {
63+
mockGetUserOrganization.mockResolvedValue({
64+
organizationId: 'org-1',
65+
role: 'member',
66+
memberId: 'member-1',
67+
})
68+
// Cancelled / past_due Team: no usable organization subscription.
69+
mockGetOrganizationSubscription.mockResolvedValue({
70+
id: 'sub-1',
71+
plan: 'team_6000',
72+
status: 'canceled',
73+
referenceId: 'org-1',
74+
})
75+
queueTableRows(member, [{ role: 'member' }])
76+
77+
const result = await getWorkspaceCreationPolicy({ userId: 'user-1' })
78+
79+
expect(result.canCreate).toBe(false)
80+
expect(result.blockedReasonCode).toBe('organization-subscription-inactive')
81+
expect(result.status).toBe(403)
82+
})
83+
84+
it('lets an owner of a lapsed organization fall back to their personal plan', async () => {
85+
mockGetUserOrganization.mockResolvedValue({
86+
organizationId: 'org-1',
87+
role: 'owner',
88+
memberId: 'member-1',
89+
})
90+
mockGetOrganizationSubscription.mockResolvedValue({
91+
id: 'sub-1',
92+
plan: 'team_6000',
93+
status: 'canceled',
94+
referenceId: 'org-1',
95+
})
96+
mockGetHighestPrioritySubscription.mockResolvedValue({
97+
id: 'sub-2',
98+
plan: 'pro_6000',
99+
status: 'active',
100+
})
101+
queueTableRows(member, [{ role: 'owner' }])
102+
queueTableRows(workspace, [{ value: 0 }])
103+
104+
const result = await getWorkspaceCreationPolicy({ userId: 'user-1' })
105+
106+
expect(result.canCreate).toBe(true)
107+
expect(result.workspaceMode).toBe(WORKSPACE_MODE.PERSONAL)
108+
expect(result.maxWorkspaces).toBe(3)
109+
// The membership snapshot lets creation tell "already a member" apart from
110+
// "joined mid-create", so the owner is not spuriously 409'd.
111+
expect(result.observedOrganizationId).toBe('org-1')
112+
})
113+
62114
it('allows pro users to create up to three personal workspaces', async () => {
63115
mockGetHighestPrioritySubscription.mockResolvedValueOnce({
64116
id: 'sub-1',

apps/sim/lib/workspaces/policy.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ export interface WorkspaceCreationPolicy {
8787
* any membership as a mid-create join.
8888
*/
8989
observedOrganizationId: string | null
90+
/** Discriminant for blocked states the workspace mode cannot distinguish. */
91+
blockedReasonCode?: 'organization-subscription-inactive'
9092
}
9193

9294
interface GetWorkspaceCreationPolicyParams {
@@ -377,6 +379,31 @@ export async function getWorkspaceCreationPolicy({
377379
observedOrganizationId: membership?.organizationId ?? null,
378380
}
379381
}
382+
383+
/**
384+
* Lapsed organization (no usable Team/Enterprise plan). A plain member
385+
* gets NO personal fallback: letting them create workspaces here would
386+
* hand them an estate outside every admin's view purely because billing
387+
* lapsed — exactly the purview escape this regime closes. Owners and
388+
* admins DO fall through to the personal regime below: they sit at the top
389+
* of the hierarchy, so there is no purview to escape, and after a
390+
* downgrade they are usually back on a personal plan they still pay for.
391+
*/
392+
if (!isOrgAdminRole(orgRole)) {
393+
return {
394+
canCreate: false,
395+
workspaceMode: WORKSPACE_MODE.ORGANIZATION,
396+
organizationId,
397+
billedAccountUserId: (await getOrganizationOwnerId(organizationId)) ?? userId,
398+
maxWorkspaces: null,
399+
currentWorkspaceCount: 0,
400+
reason:
401+
"Your organization's subscription is inactive. Ask an organization owner to reactivate it before creating workspaces.",
402+
status: 403,
403+
observedOrganizationId: membership?.organizationId ?? null,
404+
blockedReasonCode: 'organization-subscription-inactive',
405+
}
406+
}
380407
}
381408

382409
const highestPrioritySubscription = await getHighestPrioritySubscription(userId)

0 commit comments

Comments
 (0)