Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/platform/workspace/api/workspaceApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ interface Workspace {
export interface WorkspaceWithRole extends Workspace {
role: WorkspaceRole
subscription_tier?: SubscriptionTier
// ASSUMED FIELD — BE SPEC NOT FINALIZED. REVISIT (FE-770 Q3 / BE-1337).
// Presumed current-user-relative flag (like `role`): true when the current
// user is the workspace's original owner/creator. Gates the creator-only
// billing lifecycle actions (cancel / reactivate / downgrade) — see "Roles &
// Billing-Permission Gating" in the FE SSOT. The real field name/shape and the
// determination method (proposed "by creation date") are still open; until BE
// ships this, `is_creator` is undefined and lifecycle gating fails closed.
is_creator?: boolean
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

export interface Member {
Expand Down
35 changes: 35 additions & 0 deletions src/platform/workspace/composables/useWorkspaceUI.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,41 @@ describe('useWorkspaceUI', () => {
})
})

// ASSUMES /api/workspaces exposes `is_creator` — BE spec not finalized.
describe('subscription lifecycle (creator-only)', () => {
it('grants lifecycle to the personal-workspace sole owner', async () => {
mockActiveWorkspace.value = personalWorkspace
const ui = await loadComposable()
expect(ui.permissions.value.canManageSubscriptionLifecycle).toBe(true)
})

it('grants lifecycle to a team owner flagged as original owner', async () => {
mockActiveWorkspace.value = { ...teamOwnerWorkspace, is_creator: true }
const ui = await loadComposable()
expect(ui.permissions.value.canManageSubscription).toBe(true)
expect(ui.permissions.value.canManageSubscriptionLifecycle).toBe(true)
})

it('withholds lifecycle from a promoted (non-creator) team owner', async () => {
mockActiveWorkspace.value = { ...teamOwnerWorkspace, is_creator: false }
const ui = await loadComposable()
expect(ui.permissions.value.canManageSubscription).toBe(true)
expect(ui.permissions.value.canManageSubscriptionLifecycle).toBe(false)
})

it('fails closed when is_creator is absent (BE flag not shipped yet)', async () => {
mockActiveWorkspace.value = teamOwnerWorkspace
const ui = await loadComposable()
expect(ui.permissions.value.canManageSubscriptionLifecycle).toBe(false)
})

it('withholds lifecycle from members', async () => {
mockActiveWorkspace.value = teamMemberWorkspace
const ui = await loadComposable()
expect(ui.permissions.value.canManageSubscriptionLifecycle).toBe(false)
})
})

describe('shared instance', () => {
it('returns the same composable state for multiple callers within a test', async () => {
mockActiveWorkspace.value = teamOwnerWorkspace
Expand Down
26 changes: 24 additions & 2 deletions src/platform/workspace/composables/useWorkspaceUI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ interface WorkspacePermissions {
canLeaveWorkspace: boolean
canAccessWorkspaceMenu: boolean
canManageSubscription: boolean
// Creator-only subscription lifecycle: cancel / reactivate / downgrade.
// Any owner has `canManageSubscription` (manage payment, top-up, change
// commit); only the original owner gets `canManageSubscriptionLifecycle`.
canManageSubscriptionLifecycle: boolean
canTopUp: boolean
}

Expand All @@ -34,7 +38,8 @@ interface WorkspaceUIConfig {

function getPermissions(
type: WorkspaceType,
role: WorkspaceRole
role: WorkspaceRole,
isOriginalOwner: boolean
): WorkspacePermissions {
if (type === 'personal') {
return {
Expand All @@ -46,6 +51,8 @@ function getPermissions(
canLeaveWorkspace: false,
canAccessWorkspaceMenu: false,
canManageSubscription: true,
// Personal workspace is single-member: the user is the sole owner/creator.
canManageSubscriptionLifecycle: true,
canTopUp: true
}
}
Expand All @@ -60,6 +67,7 @@ function getPermissions(
canLeaveWorkspace: true,
canAccessWorkspaceMenu: true,
canManageSubscription: true,
canManageSubscriptionLifecycle: isOriginalOwner,
canTopUp: true
}
}
Expand All @@ -74,6 +82,7 @@ function getPermissions(
canLeaveWorkspace: true,
canAccessWorkspaceMenu: true,
canManageSubscription: false,
canManageSubscriptionLifecycle: false,
canTopUp: false
}
}
Expand Down Expand Up @@ -145,8 +154,21 @@ function useWorkspaceUIInternal() {
() => store.activeWorkspace?.role ?? 'owner'
)

// ASSUMED SIGNAL — BE SPEC NOT FINALIZED. REVISIT (FE-770 Q3 / BE-1337).
// Reads `is_creator` off the active workspace, presumed to come from
// /api/workspaces. Until BE ships it the flag is undefined → fails closed
// (lifecycle actions hidden for everyone in a team). See "Roles &
// Billing-Permission Gating" in the FE SSOT.
const isOriginalOwner = computed(
() => store.activeWorkspace?.is_creator ?? false
)

const permissions = computed<WorkspacePermissions>(() =>
getPermissions(workspaceType.value, workspaceRole.value)
getPermissions(
workspaceType.value,
workspaceRole.value,
isOriginalOwner.value
)
)

const uiConfig = computed<WorkspaceUIConfig>(() =>
Expand Down
Loading