diff --git a/packages/frontend-2/lib/billing/helpers/plan.ts b/packages/frontend-2/lib/billing/helpers/plan.ts index d358b6a24..6921a1aa1 100644 --- a/packages/frontend-2/lib/billing/helpers/plan.ts +++ b/packages/frontend-2/lib/billing/helpers/plan.ts @@ -26,7 +26,8 @@ export const formatName = (plan?: MaybeNullOrUndefined) => { [WorkspacePlans.TeamUnlimitedInvoiced]: 'Starter (Invoiced)', [WorkspacePlans.Pro]: 'Business', [WorkspacePlans.ProUnlimited]: 'Business', - [WorkspacePlans.ProUnlimitedInvoiced]: 'Business (Invoiced)' + [WorkspacePlans.ProUnlimitedInvoiced]: 'Business (Invoiced)', + [WorkspacePlans.Enterprise]: 'Enterprise' } return formattedPlanNames[plan] } diff --git a/packages/frontend-2/lib/common/generated/gql/graphql.ts b/packages/frontend-2/lib/common/generated/gql/graphql.ts index a0c1e5cd9..04470ac47 100644 --- a/packages/frontend-2/lib/common/generated/gql/graphql.ts +++ b/packages/frontend-2/lib/common/generated/gql/graphql.ts @@ -4931,6 +4931,7 @@ export type WorkspacePlanUsage = { export const WorkspacePlans = { Academia: 'academia', + Enterprise: 'enterprise', Free: 'free', Pro: 'pro', ProUnlimited: 'proUnlimited', diff --git a/packages/server/assets/gatekeeperCore/typedefs/gatekeeper.graphql b/packages/server/assets/gatekeeperCore/typedefs/gatekeeper.graphql index 5da6a5299..922f8b15a 100644 --- a/packages/server/assets/gatekeeperCore/typedefs/gatekeeper.graphql +++ b/packages/server/assets/gatekeeperCore/typedefs/gatekeeper.graphql @@ -64,6 +64,7 @@ enum WorkspacePlans { pro proUnlimited proUnlimitedInvoiced + enterprise } enum WorkspacePlanStatuses { diff --git a/packages/server/modules/cli/commands/workspaces/set-plan.ts b/packages/server/modules/cli/commands/workspaces/set-plan.ts index 796885567..4a614e96a 100644 --- a/packages/server/modules/cli/commands/workspaces/set-plan.ts +++ b/packages/server/modules/cli/commands/workspaces/set-plan.ts @@ -10,7 +10,7 @@ import { upsertWorkspacePlanFactory } from '@/modules/gatekeeper/repositories/billing' import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace' -import { PaidWorkspacePlans, PaidWorkspacePlanStatuses } from '@speckle/shared' +import { WorkspacePlans, WorkspacePlanStatuses } from '@speckle/shared' import { getEventBus } from '@/modules/shared/services/eventBus' import { updateWorkspacePlanFactory } from '@/modules/gatekeeper/services/workspacePlans' @@ -18,8 +18,9 @@ const command: CommandModule< unknown, { workspaceSlugOrId: string - status: PaidWorkspacePlanStatuses - plan: PaidWorkspacePlans + // you need to know what you are doing, status and plan pairing validity is not ensured here + status: WorkspacePlanStatuses + plan: WorkspacePlans } > = { command: 'set-plan [plan] [status]', @@ -32,8 +33,8 @@ const command: CommandModule< plan: { describe: 'Plan to set the status for', type: 'string', - default: PaidWorkspacePlans.Team, - choices: [PaidWorkspacePlans.Team, PaidWorkspacePlans.Pro] + default: WorkspacePlans.Team, + choices: Object.values(WorkspacePlans) }, status: { describe: 'Status to set for the workspace plan', diff --git a/packages/server/modules/core/graph/generated/graphql.ts b/packages/server/modules/core/graph/generated/graphql.ts index 26edcbea4..43a630078 100644 --- a/packages/server/modules/core/graph/generated/graphql.ts +++ b/packages/server/modules/core/graph/generated/graphql.ts @@ -4954,6 +4954,7 @@ export type WorkspacePlanUsage = { export const WorkspacePlans = { Academia: 'academia', + Enterprise: 'enterprise', Free: 'free', Pro: 'pro', ProUnlimited: 'proUnlimited', diff --git a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts index 6ecb35a6b..9bef4b80b 100644 --- a/packages/server/modules/cross-server-sync/graph/generated/graphql.ts +++ b/packages/server/modules/cross-server-sync/graph/generated/graphql.ts @@ -4934,6 +4934,7 @@ export type WorkspacePlanUsage = { export const WorkspacePlans = { Academia: 'academia', + Enterprise: 'enterprise', Free: 'free', Pro: 'pro', ProUnlimited: 'proUnlimited', diff --git a/packages/server/modules/gatekeeper/graph/resolvers/index.ts b/packages/server/modules/gatekeeper/graph/resolvers/index.ts index 14d9368b8..932a9788d 100644 --- a/packages/server/modules/gatekeeper/graph/resolvers/index.ts +++ b/packages/server/modules/gatekeeper/graph/resolvers/index.ts @@ -1,7 +1,12 @@ import { getFeatureFlags, getFrontendOrigin } from '@/modules/shared/helpers/envHelper' import type { Resolvers } from '@/modules/core/graph/generated/graphql' import { authorizeResolver } from '@/modules/shared' -import { Roles, throwUncoveredError, WorkspacePlanFeatures } from '@speckle/shared' +import { + Roles, + throwUncoveredError, + WorkspacePlanFeatures, + WorkspacePlans +} from '@speckle/shared' import { getWorkspaceFactory, getWorkspaceRoleForUserFactory, @@ -71,19 +76,20 @@ export = FF_GATEKEEPER_MODULE_ENABLED if (!workspacePlan) return null let paymentMethod: WorkspacePaymentMethod switch (workspacePlan.name) { - case 'team': - case 'teamUnlimited': - case 'pro': - case 'proUnlimited': + case WorkspacePlans.Team: + case WorkspacePlans.TeamUnlimited: + case WorkspacePlans.Pro: + case WorkspacePlans.ProUnlimited: paymentMethod = WorkspacePaymentMethod.Billing break - case 'unlimited': - case 'academia': - case 'free': + case WorkspacePlans.Unlimited: + case WorkspacePlans.Academia: + case WorkspacePlans.Free: paymentMethod = WorkspacePaymentMethod.Unpaid break - case 'proUnlimitedInvoiced': - case 'teamUnlimitedInvoiced': + case WorkspacePlans.ProUnlimitedInvoiced: + case WorkspacePlans.TeamUnlimitedInvoiced: + case WorkspacePlans.Enterprise: paymentMethod = WorkspacePaymentMethod.Invoice break default: @@ -248,17 +254,18 @@ export = FF_GATEKEEPER_MODULE_ENABLED if (subscription) { let purchased = 0 switch (workspacePlan.name) { - case 'unlimited': - case 'academia': - case 'free': - case 'proUnlimitedInvoiced': - case 'teamUnlimitedInvoiced': + case WorkspacePlans.Unlimited: + case WorkspacePlans.Academia: + case WorkspacePlans.Free: + case WorkspacePlans.ProUnlimitedInvoiced: + case WorkspacePlans.TeamUnlimitedInvoiced: + case WorkspacePlans.Enterprise: // not stripe paid plans and old plans do not have seats available break - case 'team': - case 'teamUnlimited': - case 'pro': - case 'proUnlimited': + case WorkspacePlans.Team: + case WorkspacePlans.TeamUnlimited: + case WorkspacePlans.Pro: + case WorkspacePlans.ProUnlimited: purchased = getTotalSeatsCountByPlanFactory({ getWorkspacePlanProductId })({ diff --git a/packages/server/modules/gatekeeper/services/subscriptions.ts b/packages/server/modules/gatekeeper/services/subscriptions.ts index 9647979a7..c67818b76 100644 --- a/packages/server/modules/gatekeeper/services/subscriptions.ts +++ b/packages/server/modules/gatekeeper/services/subscriptions.ts @@ -19,7 +19,8 @@ import { import { PaidWorkspacePlans, PaidWorkspacePlanStatuses, - throwUncoveredError + throwUncoveredError, + WorkspacePlans } from '@speckle/shared' import { cloneDeep } from 'lodash' import { CountSeatsByTypeInWorkspace } from '@/modules/gatekeeper/domain/operations' @@ -78,16 +79,17 @@ export const handleSubscriptionUpdateFactory = if (status) { switch (workspacePlan.name) { - case 'team': - case 'teamUnlimited': - case 'pro': - case 'proUnlimited': + case WorkspacePlans.Team: + case WorkspacePlans.TeamUnlimited: + case WorkspacePlans.Pro: + case WorkspacePlans.ProUnlimited: break - case 'unlimited': - case 'academia': - case 'proUnlimitedInvoiced': - case 'teamUnlimitedInvoiced': - case 'free': + case WorkspacePlans.Unlimited: + case WorkspacePlans.Academia: + case WorkspacePlans.ProUnlimitedInvoiced: + case WorkspacePlans.TeamUnlimitedInvoiced: + case WorkspacePlans.Free: + case WorkspacePlans.Enterprise: throw new WorkspacePlanMismatchError() default: throwUncoveredError(workspacePlan) @@ -146,21 +148,22 @@ export const addWorkspaceSubscriptionSeatIfNeededFactory = // if (!workspaceSubscription) throw new WorkspaceSubscriptionNotFoundError() switch (workspacePlan.name) { - case 'team': - case 'teamUnlimited': - case 'pro': - case 'proUnlimited': + case WorkspacePlans.Team: + case WorkspacePlans.TeamUnlimited: + case WorkspacePlans.Pro: + case WorkspacePlans.ProUnlimited: // If viewer seat type, we don't need to do anything if (seatType === WorkspaceSeatType.Viewer) { return } else { break } - case 'unlimited': - case 'academia': - case 'proUnlimitedInvoiced': - case 'teamUnlimitedInvoiced': - case 'free': + case WorkspacePlans.Unlimited: + case WorkspacePlans.Academia: + case WorkspacePlans.ProUnlimitedInvoiced: + case WorkspacePlans.TeamUnlimitedInvoiced: + case WorkspacePlans.Free: + case WorkspacePlans.Enterprise: throw new WorkspacePlanMismatchError() default: throwUncoveredError(workspacePlan) diff --git a/packages/server/modules/gatekeeper/services/subscriptions/manageSubscriptionDownscale.ts b/packages/server/modules/gatekeeper/services/subscriptions/manageSubscriptionDownscale.ts index b26bf3902..55fb567ba 100644 --- a/packages/server/modules/gatekeeper/services/subscriptions/manageSubscriptionDownscale.ts +++ b/packages/server/modules/gatekeeper/services/subscriptions/manageSubscriptionDownscale.ts @@ -15,7 +15,7 @@ import { } from '@/modules/gatekeeper/errors/billing' import { mutateSubscriptionDataWithNewValidSeatNumbers } from '@/modules/gatekeeper/services/subscriptions/mutateSubscriptionDataWithNewValidSeatNumbers' import { Logger } from '@/observability/logging' -import { throwUncoveredError } from '@speckle/shared' +import { throwUncoveredError, WorkspacePlans } from '@speckle/shared' import { cloneDeep, isEqual } from 'lodash' type DownscaleWorkspaceSubscription = (args: { @@ -41,16 +41,17 @@ export const downscaleWorkspaceSubscriptionFactory = if (!workspacePlan) throw new WorkspacePlanNotFoundError() switch (workspacePlan.name) { - case 'team': - case 'teamUnlimited': - case 'pro': - case 'proUnlimited': + case WorkspacePlans.Team: + case WorkspacePlans.TeamUnlimited: + case WorkspacePlans.Pro: + case WorkspacePlans.ProUnlimited: break - case 'unlimited': - case 'academia': - case 'proUnlimitedInvoiced': - case 'teamUnlimitedInvoiced': - case 'free': + case WorkspacePlans.Free: + case WorkspacePlans.Academia: + case WorkspacePlans.ProUnlimitedInvoiced: + case WorkspacePlans.TeamUnlimitedInvoiced: + case WorkspacePlans.Enterprise: + case WorkspacePlans.Unlimited: throw new WorkspacePlanMismatchError() default: throwUncoveredError(workspacePlan) diff --git a/packages/server/modules/gatekeeper/services/subscriptions/upgradeWorkspaceSubscription.ts b/packages/server/modules/gatekeeper/services/subscriptions/upgradeWorkspaceSubscription.ts index 5eaade669..9f1e646e5 100644 --- a/packages/server/modules/gatekeeper/services/subscriptions/upgradeWorkspaceSubscription.ts +++ b/packages/server/modules/gatekeeper/services/subscriptions/upgradeWorkspaceSubscription.ts @@ -27,7 +27,8 @@ import { EventBusEmit } from '@/modules/shared/services/eventBus' import { PaidWorkspacePlans, throwUncoveredError, - WorkspacePlanBillingIntervals + WorkspacePlanBillingIntervals, + WorkspacePlans } from '@speckle/shared' import { cloneDeep } from 'lodash' @@ -68,16 +69,17 @@ export const upgradeWorkspaceSubscriptionFactory = if (!workspacePlan) throw new WorkspacePlanNotFoundError() switch (workspacePlan.name) { - case 'unlimited': - case 'academia': - case 'teamUnlimitedInvoiced': - case 'proUnlimitedInvoiced': - case 'free': // Upgrade from free is handled through startCheckout since it is from free to paid + case WorkspacePlans.Unlimited: + case WorkspacePlans.Academia: + case WorkspacePlans.TeamUnlimitedInvoiced: + case WorkspacePlans.ProUnlimitedInvoiced: + case WorkspacePlans.Enterprise: + case WorkspacePlans.Free: // Upgrade from free is handled through startCheckout since it is from free to paid throw new WorkspaceNotPaidPlanError() - case 'team': - case 'teamUnlimited': - case 'pro': - case 'proUnlimited': + case WorkspacePlans.Team: + case WorkspacePlans.TeamUnlimited: + case WorkspacePlans.Pro: + case WorkspacePlans.ProUnlimited: break default: throwUncoveredError(workspacePlan) diff --git a/packages/server/modules/gatekeeper/services/upgrades.ts b/packages/server/modules/gatekeeper/services/upgrades.ts index 607e3537a..8f3a93eea 100644 --- a/packages/server/modules/gatekeeper/services/upgrades.ts +++ b/packages/server/modules/gatekeeper/services/upgrades.ts @@ -9,7 +9,8 @@ const WorkspacePlansUpgradeMapping: Record = { teamUnlimitedInvoiced: [], pro: ['pro', 'proUnlimited'], proUnlimited: ['proUnlimited'], - proUnlimitedInvoiced: [] + proUnlimitedInvoiced: [], + enterprise: [] } export const isUpgradeWorkspacePlanValid = ({ diff --git a/packages/server/modules/gatekeeper/services/workspacePlans.ts b/packages/server/modules/gatekeeper/services/workspacePlans.ts index 117f2c88a..281189d3d 100644 --- a/packages/server/modules/gatekeeper/services/workspacePlans.ts +++ b/packages/server/modules/gatekeeper/services/workspacePlans.ts @@ -6,7 +6,7 @@ import { InvalidWorkspacePlanStatus } from '@/modules/gatekeeper/errors/billing' import { EventBusEmit } from '@/modules/shared/services/eventBus' import { GetWorkspace } from '@/modules/workspaces/domain/operations' import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace' -import { throwUncoveredError, WorkspacePlan } from '@speckle/shared' +import { throwUncoveredError, WorkspacePlan, WorkspacePlans } from '@speckle/shared' export const updateWorkspacePlanFactory = ({ @@ -35,10 +35,10 @@ export const updateWorkspacePlanFactory = const createdAt = new Date() const updatedAt = new Date() switch (name) { - case 'team': - case 'teamUnlimited': - case 'pro': - case 'proUnlimited': + case WorkspacePlans.Team: + case WorkspacePlans.TeamUnlimited: + case WorkspacePlans.Pro: + case WorkspacePlans.ProUnlimited: switch (status) { case 'valid': case 'cancelationScheduled': @@ -53,11 +53,12 @@ export const updateWorkspacePlanFactory = } break - case 'free': - case 'academia': - case 'unlimited': - case 'teamUnlimitedInvoiced': - case 'proUnlimitedInvoiced': + case WorkspacePlans.Free: + case WorkspacePlans.Academia: + case WorkspacePlans.Unlimited: + case WorkspacePlans.Enterprise: + case WorkspacePlans.TeamUnlimitedInvoiced: + case WorkspacePlans.ProUnlimitedInvoiced: switch (status) { case 'valid': await upsertWorkspacePlan({ diff --git a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts index f836a655b..250ae9358 100644 --- a/packages/server/modules/workspaces/graph/resolvers/workspaces.ts +++ b/packages/server/modules/workspaces/graph/resolvers/workspaces.ts @@ -116,6 +116,7 @@ import { } from '@/modules/workspaces/services/retrieval' import { Roles, + WorkspacePlans, WorkspaceRoles, removeNullOrUndefinedKeys, throwUncoveredError @@ -707,10 +708,10 @@ export = FF_WORKSPACES_MODULE_ENABLED const workspacePlan = await getWorkspacePlanFactory({ db })({ workspaceId }) if (workspacePlan) { switch (workspacePlan.name) { - case 'team': - case 'teamUnlimited': - case 'pro': - case 'proUnlimited': + case WorkspacePlans.Team: + case WorkspacePlans.TeamUnlimited: + case WorkspacePlans.Pro: + case WorkspacePlans.ProUnlimited: switch (workspacePlan.status) { case 'cancelationScheduled': case 'valid': @@ -721,11 +722,12 @@ export = FF_WORKSPACES_MODULE_ENABLED default: throwUncoveredError(workspacePlan) } - case 'free': - case 'unlimited': - case 'academia': - case 'proUnlimitedInvoiced': - case 'teamUnlimitedInvoiced': + case WorkspacePlans.Free: + case WorkspacePlans.Unlimited: + case WorkspacePlans.Academia: + case WorkspacePlans.Enterprise: + case WorkspacePlans.ProUnlimitedInvoiced: + case WorkspacePlans.TeamUnlimitedInvoiced: break default: throwUncoveredError(workspacePlan) diff --git a/packages/server/test/graphql/generated/graphql.ts b/packages/server/test/graphql/generated/graphql.ts index 675a6460f..c3d50f319 100644 --- a/packages/server/test/graphql/generated/graphql.ts +++ b/packages/server/test/graphql/generated/graphql.ts @@ -4935,6 +4935,7 @@ export type WorkspacePlanUsage = { export const WorkspacePlans = { Academia: 'academia', + Enterprise: 'enterprise', Free: 'free', Pro: 'pro', ProUnlimited: 'proUnlimited', diff --git a/packages/shared/src/workspaces/helpers/features.ts b/packages/shared/src/workspaces/helpers/features.ts index 3aee0ffc6..87bb14911 100644 --- a/packages/shared/src/workspaces/helpers/features.ts +++ b/packages/shared/src/workspaces/helpers/features.ts @@ -148,6 +148,17 @@ export const WorkspacePaidPlanConfigs: { export const WorkspaceUnpaidPlanConfigs: { [plan in UnpaidWorkspacePlans]: WorkspacePlanConfig } = { + [UnpaidWorkspacePlans.Enterprise]: { + plan: UnpaidWorkspacePlans.Enterprise, + features: [ + ...baseFeatures, + WorkspacePlanFeatures.DomainSecurity, + WorkspacePlanFeatures.SSO, + WorkspacePlanFeatures.CustomDataRegion, + WorkspacePlanFeatures.HideSpeckleBranding + ], + limits: unlimited + }, [UnpaidWorkspacePlans.Unlimited]: { plan: UnpaidWorkspacePlans.Unlimited, features: [ diff --git a/packages/shared/src/workspaces/helpers/plans.spec.ts b/packages/shared/src/workspaces/helpers/plans.spec.ts index e65b5d037..4158a8cf5 100644 --- a/packages/shared/src/workspaces/helpers/plans.spec.ts +++ b/packages/shared/src/workspaces/helpers/plans.spec.ts @@ -18,7 +18,8 @@ describe('plan helpers', () => { proUnlimitedInvoiced: false, team: false, teamUnlimited: true, - teamUnlimitedInvoiced: false + teamUnlimitedInvoiced: false, + enterprise: false } it.each(Object.entries(planCases))( 'plan %s include the paid unlimited projects addon -> %s', @@ -41,7 +42,8 @@ describe('plan helpers', () => { proUnlimitedInvoiced: false, team: true, teamUnlimited: true, - teamUnlimitedInvoiced: false + teamUnlimitedInvoiced: false, + enterprise: false } it.each(Object.entries(planCases))( 'is plan %s available self served -> %s', diff --git a/packages/shared/src/workspaces/helpers/plans.ts b/packages/shared/src/workspaces/helpers/plans.ts index 1b4523bd2..440e7b360 100644 --- a/packages/shared/src/workspaces/helpers/plans.ts +++ b/packages/shared/src/workspaces/helpers/plans.ts @@ -13,6 +13,7 @@ export type PaidWorkspacePlans = export const UnpaidWorkspacePlans = { TeamUnlimitedInvoiced: 'teamUnlimitedInvoiced', ProUnlimitedInvoiced: 'proUnlimitedInvoiced', + Enterprise: 'enterprise', Unlimited: 'unlimited', Academia: 'academia', Free: 'free' @@ -32,16 +33,17 @@ export const doesPlanIncludeUnlimitedProjectsAddon = ( plan: WorkspacePlans ): boolean => { switch (plan) { - case 'teamUnlimited': - case 'proUnlimited': + case WorkspacePlans.TeamUnlimited: + case WorkspacePlans.ProUnlimited: return true - case 'free': - case 'team': - case 'pro': - case 'teamUnlimitedInvoiced': - case 'proUnlimitedInvoiced': - case 'unlimited': - case 'academia': + case WorkspacePlans.Free: + case WorkspacePlans.Team: + case WorkspacePlans.Pro: + case WorkspacePlans.TeamUnlimitedInvoiced: + case WorkspacePlans.ProUnlimitedInvoiced: + case WorkspacePlans.Unlimited: + case WorkspacePlans.Academia: + case WorkspacePlans.Enterprise: return false default: @@ -51,16 +53,17 @@ export const doesPlanIncludeUnlimitedProjectsAddon = ( export const isSelfServeAvailablePlan = (plan: WorkspacePlans): boolean => { switch (plan) { - case 'free': - case 'team': - case 'teamUnlimited': - case 'pro': - case 'proUnlimited': + case WorkspacePlans.Free: + case WorkspacePlans.Team: + case WorkspacePlans.TeamUnlimited: + case WorkspacePlans.Pro: + case WorkspacePlans.ProUnlimited: return true - case 'teamUnlimitedInvoiced': - case 'proUnlimitedInvoiced': - case 'unlimited': - case 'academia': + case WorkspacePlans.TeamUnlimitedInvoiced: + case WorkspacePlans.ProUnlimitedInvoiced: + case WorkspacePlans.Unlimited: + case WorkspacePlans.Academia: + case WorkspacePlans.Enterprise: return false default: @@ -70,16 +73,17 @@ export const isSelfServeAvailablePlan = (plan: WorkspacePlans): boolean => { export const isPaidPlan = (plan: WorkspacePlans): boolean => { switch (plan) { - case 'team': - case 'teamUnlimited': - case 'pro': - case 'proUnlimited': + case WorkspacePlans.Team: + case WorkspacePlans.TeamUnlimited: + case WorkspacePlans.Pro: + case WorkspacePlans.ProUnlimited: return true - case 'free': - case 'teamUnlimitedInvoiced': - case 'proUnlimitedInvoiced': - case 'unlimited': - case 'academia': + case WorkspacePlans.Free: + case WorkspacePlans.TeamUnlimitedInvoiced: + case WorkspacePlans.ProUnlimitedInvoiced: + case WorkspacePlans.Unlimited: + case WorkspacePlans.Academia: + case WorkspacePlans.Enterprise: return false default: