chore: get rid of all old workspace plan code (#4624)
* first batch of changes * tests fix * FE fixed * renaming constants * test fixes * moar test fixes * another test fix * reenable app rover check --------- Co-authored-by: Gergő Jedlicska <gergo@jedlicska.com>
This commit is contained in:
committed by
GitHub
parent
76b84e2068
commit
211922b6a6
@@ -541,10 +541,10 @@ jobs:
|
||||
command: 'IGNORE_MISSING_MIRATIONS=true yarn cli graphql introspect'
|
||||
working_directory: 'packages/server'
|
||||
|
||||
# - run:
|
||||
# name: Checking for GQL schema breakages against app.speckle.systems
|
||||
# command: 'yarn rover graph check Speckle-Server@app-speckle-systems --schema ./introspected-schema.graphql'
|
||||
# working_directory: 'packages/server'
|
||||
- run:
|
||||
name: Checking for GQL schema breakages against app.speckle.systems
|
||||
command: 'yarn rover graph check Speckle-Server@app-speckle-systems --schema ./introspected-schema.graphql'
|
||||
working_directory: 'packages/server'
|
||||
|
||||
- run:
|
||||
name: Checking for GQL schema breakages against latest.speckle.systems
|
||||
|
||||
@@ -1913,11 +1913,8 @@ export type OnboardingCompletionInput = {
|
||||
};
|
||||
|
||||
export enum PaidWorkspacePlans {
|
||||
Business = 'business',
|
||||
Plus = 'plus',
|
||||
Pro = 'pro',
|
||||
ProUnlimited = 'proUnlimited',
|
||||
Starter = 'starter',
|
||||
Team = 'team',
|
||||
TeamUnlimited = 'teamUnlimited'
|
||||
}
|
||||
@@ -4848,9 +4845,7 @@ export type WorkspacePlanPrice = {
|
||||
export enum WorkspacePlanStatuses {
|
||||
CancelationScheduled = 'cancelationScheduled',
|
||||
Canceled = 'canceled',
|
||||
Expired = 'expired',
|
||||
PaymentFailed = 'paymentFailed',
|
||||
Trial = 'trial',
|
||||
Valid = 'valid'
|
||||
}
|
||||
|
||||
@@ -4862,16 +4857,10 @@ export type WorkspacePlanUsage = {
|
||||
|
||||
export enum WorkspacePlans {
|
||||
Academia = 'academia',
|
||||
Business = 'business',
|
||||
BusinessInvoiced = 'businessInvoiced',
|
||||
Free = 'free',
|
||||
Plus = 'plus',
|
||||
PlusInvoiced = 'plusInvoiced',
|
||||
Pro = 'pro',
|
||||
ProUnlimited = 'proUnlimited',
|
||||
ProUnlimitedInvoiced = 'proUnlimitedInvoiced',
|
||||
Starter = 'starter',
|
||||
StarterInvoiced = 'starterInvoiced',
|
||||
Team = 'team',
|
||||
TeamUnlimited = 'teamUnlimited',
|
||||
TeamUnlimitedInvoiced = 'teamUnlimitedInvoiced',
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
:workspace-id="props.workspaceId"
|
||||
:has-subscription="!!subscription"
|
||||
:currency="props.currency"
|
||||
@on-upgrade-click="toggleUpgradeDialog(plan as PaidWorkspacePlansNew)"
|
||||
@on-upgrade-click="toggleUpgradeDialog(plan as PaidWorkspacePlans)"
|
||||
/>
|
||||
|
||||
<SettingsWorkspacesBillingUpgradeDialog
|
||||
@@ -32,7 +32,7 @@
|
||||
import { BillingInterval, type Currency } from '~/lib/common/generated/gql/graphql'
|
||||
import {
|
||||
WorkspacePlans,
|
||||
type PaidWorkspacePlansNew,
|
||||
type PaidWorkspacePlans,
|
||||
type MaybeNullOrUndefined,
|
||||
type WorkspaceRoles,
|
||||
Roles
|
||||
@@ -59,7 +59,7 @@ const {
|
||||
const mixpanel = useMixpanel()
|
||||
|
||||
const isUpgradeDialogOpen = ref(false)
|
||||
const planToUpgrade = ref<PaidWorkspacePlansNew | null>(null)
|
||||
const planToUpgrade = ref<PaidWorkspacePlans | null>(null)
|
||||
|
||||
const plans = computed(() => [
|
||||
WorkspacePlans.Free,
|
||||
@@ -69,7 +69,7 @@ const plans = computed(() => [
|
||||
|
||||
const isAdmin = computed(() => props.role === Roles.Workspace.Admin)
|
||||
|
||||
const toggleUpgradeDialog = (plan: PaidWorkspacePlansNew) => {
|
||||
const toggleUpgradeDialog = (plan: PaidWorkspacePlans) => {
|
||||
planToUpgrade.value = plan
|
||||
isUpgradeDialogOpen.value = !isUpgradeDialogOpen.value
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
import { useWorkspacePlan } from '~~/lib/workspaces/composables/plan'
|
||||
import { useWorkspaceAddonPrices } from '~/lib/billing/composables/prices'
|
||||
import { formatPrice } from '~/lib/billing/helpers/plan'
|
||||
import { PaidWorkspacePlansNew, type MaybeNullOrUndefined } from '@speckle/shared'
|
||||
import { PaidWorkspacePlans, type MaybeNullOrUndefined } from '@speckle/shared'
|
||||
import { BillingInterval, Currency } from '~/lib/common/generated/gql/graphql'
|
||||
import { useActiveWorkspace } from '~/lib/workspaces/composables/activeWorkspace'
|
||||
import { useMixpanel } from '~~/lib/core/composables/mp'
|
||||
@@ -101,15 +101,15 @@ const unlimitedAddOnButton = computed(() => ({
|
||||
}))
|
||||
|
||||
const planToUpgrade = computed(() => {
|
||||
return plan.value?.name === PaidWorkspacePlansNew.Team
|
||||
? PaidWorkspacePlansNew.TeamUnlimited
|
||||
: PaidWorkspacePlansNew.ProUnlimited
|
||||
return plan.value?.name === PaidWorkspacePlans.Team
|
||||
? PaidWorkspacePlans.TeamUnlimited
|
||||
: PaidWorkspacePlans.ProUnlimited
|
||||
})
|
||||
|
||||
const addonPrice = computed(() => {
|
||||
if (!plan.value) return null
|
||||
const addonPrice =
|
||||
addonPrices.value?.[currency.value]?.[plan.value.name as PaidWorkspacePlansNew]
|
||||
addonPrices.value?.[currency.value]?.[plan.value.name as PaidWorkspacePlans]
|
||||
if (!addonPrice) return null
|
||||
|
||||
return formatPrice({
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useWorkspacePlan } from '~/lib/workspaces/composables/plan'
|
||||
import type { PaidWorkspacePlansNew } from '@speckle/shared'
|
||||
import type { PaidWorkspacePlans } from '@speckle/shared'
|
||||
import { BillingInterval } from '~/lib/common/generated/gql/graphql'
|
||||
import { formatPrice } from '~/lib/billing/helpers/plan'
|
||||
import { useWorkspaceAddonPrices } from '~/lib/billing/composables/prices'
|
||||
@@ -16,7 +16,7 @@ type AddonIncludedSelect = 'yes' | 'no'
|
||||
|
||||
const props = defineProps<{
|
||||
slug: string
|
||||
plan: PaidWorkspacePlansNew
|
||||
plan: PaidWorkspacePlans
|
||||
billingInterval: BillingInterval
|
||||
enableNoOption: boolean
|
||||
}>()
|
||||
|
||||
+5
-6
@@ -134,13 +134,13 @@ import {
|
||||
WorkspacePlans,
|
||||
isPaidPlan,
|
||||
doesPlanIncludeUnlimitedProjectsAddon,
|
||||
type PaidWorkspacePlansNew
|
||||
type PaidWorkspacePlans
|
||||
} from '@speckle/shared'
|
||||
import { BillingInterval } from '~/lib/common/generated/gql/graphql'
|
||||
|
||||
const props = defineProps<{
|
||||
slug: string
|
||||
plan: PaidWorkspacePlansNew
|
||||
plan: PaidWorkspacePlans
|
||||
billingInterval: BillingInterval
|
||||
editorSeatCount: number
|
||||
}>()
|
||||
@@ -176,8 +176,7 @@ const currentEditorPrice = computed(() => {
|
||||
})
|
||||
}
|
||||
|
||||
const planPrice =
|
||||
activeWorkspacePrices.value?.[plan.value.name as PaidWorkspacePlansNew]
|
||||
const planPrice = activeWorkspacePrices.value?.[plan.value.name as PaidWorkspacePlans]
|
||||
if (!planPrice) return null
|
||||
|
||||
return formatPrice({
|
||||
@@ -222,7 +221,7 @@ const newEditorPrice = computed(() => {
|
||||
|
||||
const totalPrice = computed(() => {
|
||||
const planPrice =
|
||||
activeWorkspacePrices.value?.[plan.value?.name as PaidWorkspacePlansNew]
|
||||
activeWorkspacePrices.value?.[plan.value?.name as PaidWorkspacePlans]
|
||||
if (!planPrice) return null
|
||||
|
||||
return formatPrice({
|
||||
@@ -253,7 +252,7 @@ const newTotalPriceFormatted = computed(() => {
|
||||
const currentAddonPrice = computed(() => {
|
||||
if (!plan.value?.name) return null
|
||||
const addonPrice =
|
||||
addonPrices.value?.[currency.value]?.[plan.value.name as PaidWorkspacePlansNew]
|
||||
addonPrices.value?.[currency.value]?.[plan.value.name as PaidWorkspacePlans]
|
||||
if (!addonPrice) return null
|
||||
|
||||
return intervalIsYearly.value
|
||||
|
||||
+5
-5
@@ -27,7 +27,7 @@
|
||||
import type { LayoutDialogButton } from '@speckle/ui-components'
|
||||
import { useBillingActions } from '~/lib/billing/composables/actions'
|
||||
import {
|
||||
PaidWorkspacePlansNew,
|
||||
PaidWorkspacePlans,
|
||||
WorkspacePlanConfigs,
|
||||
type MaybeNullOrUndefined,
|
||||
doesPlanIncludeUnlimitedProjectsAddon
|
||||
@@ -40,7 +40,7 @@ import { useMixpanel } from '~/lib/core/composables/mp'
|
||||
type AddonIncludedSelect = 'yes' | 'no'
|
||||
|
||||
const props = defineProps<{
|
||||
plan: PaidWorkspacePlansNew
|
||||
plan: PaidWorkspacePlans
|
||||
billingInterval: BillingInterval
|
||||
workspaceId: MaybeNullOrUndefined<string>
|
||||
slug: string
|
||||
@@ -100,9 +100,9 @@ const isSamePlanWithAddon = computed(
|
||||
// If the user has selected to include the add-on, return the new plan with the add-on
|
||||
const finalNewPlan = computed(() => {
|
||||
if (includeUnlimitedAddon.value === 'yes') {
|
||||
return props.plan === PaidWorkspacePlansNew.Team
|
||||
? PaidWorkspacePlansNew.TeamUnlimited
|
||||
: PaidWorkspacePlansNew.ProUnlimited
|
||||
return props.plan === PaidWorkspacePlans.Team
|
||||
? PaidWorkspacePlans.TeamUnlimited
|
||||
: PaidWorkspacePlans.ProUnlimited
|
||||
}
|
||||
|
||||
return props.plan
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useWorkspacesWizard } from '~/lib/workspaces/composables/wizard'
|
||||
import { useMixpanel } from '~/lib/core/composables/mp'
|
||||
import { type PaidWorkspacePlansNew, WorkspacePlans } from '@speckle/shared'
|
||||
import { type PaidWorkspacePlans, WorkspacePlans } from '@speckle/shared'
|
||||
import { useWorkspaceAddonPrices } from '~/lib/billing/composables/prices'
|
||||
import { Currency, BillingInterval } from '~/lib/common/generated/gql/graphql'
|
||||
import { formatPrice } from '~/lib/billing/helpers/plan'
|
||||
@@ -46,7 +46,7 @@ const includeUnlimitedAddon = ref<AddonIncludedSelect | undefined>(undefined)
|
||||
const addOnPrice = computed(() => {
|
||||
if (!state.value.plan) return null
|
||||
const price =
|
||||
addonPrices.value?.[Currency.Usd]?.[state.value.plan as PaidWorkspacePlansNew]
|
||||
addonPrices.value?.[Currency.Usd]?.[state.value.plan as PaidWorkspacePlans]
|
||||
if (!price) return null
|
||||
|
||||
return formatPrice({
|
||||
|
||||
@@ -20,12 +20,6 @@ export const formatName = (plan?: MaybeNullOrUndefined<WorkspacePlans>) => {
|
||||
const formattedPlanNames: Record<WorkspacePlans, string> = {
|
||||
[WorkspacePlans.Unlimited]: 'Unlimited',
|
||||
[WorkspacePlans.Academia]: 'Academia',
|
||||
[WorkspacePlans.StarterInvoiced]: 'Starter (invoiced)',
|
||||
[WorkspacePlans.PlusInvoiced]: 'Plus (Invoiced)',
|
||||
[WorkspacePlans.BusinessInvoiced]: 'Business (Invoiced)',
|
||||
[WorkspacePlans.Starter]: 'Starter',
|
||||
[WorkspacePlans.Plus]: 'Plus',
|
||||
[WorkspacePlans.Business]: 'Business',
|
||||
[WorkspacePlans.Free]: 'Free',
|
||||
[WorkspacePlans.Team]: 'Starter',
|
||||
[WorkspacePlans.TeamUnlimited]: 'Starter',
|
||||
|
||||
@@ -1919,11 +1919,8 @@ export type OnboardingCompletionInput = {
|
||||
};
|
||||
|
||||
export const PaidWorkspacePlans = {
|
||||
Business: 'business',
|
||||
Plus: 'plus',
|
||||
Pro: 'pro',
|
||||
ProUnlimited: 'proUnlimited',
|
||||
Starter: 'starter',
|
||||
Team: 'team',
|
||||
TeamUnlimited: 'teamUnlimited'
|
||||
} as const;
|
||||
@@ -4878,9 +4875,7 @@ export type WorkspacePlanPrice = {
|
||||
export const WorkspacePlanStatuses = {
|
||||
CancelationScheduled: 'cancelationScheduled',
|
||||
Canceled: 'canceled',
|
||||
Expired: 'expired',
|
||||
PaymentFailed: 'paymentFailed',
|
||||
Trial: 'trial',
|
||||
Valid: 'valid'
|
||||
} as const;
|
||||
|
||||
@@ -4893,16 +4888,10 @@ export type WorkspacePlanUsage = {
|
||||
|
||||
export const WorkspacePlans = {
|
||||
Academia: 'academia',
|
||||
Business: 'business',
|
||||
BusinessInvoiced: 'businessInvoiced',
|
||||
Free: 'free',
|
||||
Plus: 'plus',
|
||||
PlusInvoiced: 'plusInvoiced',
|
||||
Pro: 'pro',
|
||||
ProUnlimited: 'proUnlimited',
|
||||
ProUnlimitedInvoiced: 'proUnlimitedInvoiced',
|
||||
Starter: 'starter',
|
||||
StarterInvoiced: 'starterInvoiced',
|
||||
Team: 'team',
|
||||
TeamUnlimited: 'teamUnlimited',
|
||||
TeamUnlimitedInvoiced: 'teamUnlimitedInvoiced',
|
||||
|
||||
@@ -154,9 +154,7 @@ export const useSettingsMembersActions = (params: {
|
||||
workspaceSlug: params.workspaceSlug.value || ''
|
||||
})
|
||||
|
||||
const { statusIsExpired, statusIsCanceled } = useWorkspacePlan(
|
||||
params.workspaceSlug.value || ''
|
||||
)
|
||||
const { statusIsCanceled } = useWorkspacePlan(params.workspaceSlug.value || '')
|
||||
|
||||
const targetUserRole = computed(() => {
|
||||
return params.targetUser.value.role
|
||||
@@ -247,8 +245,8 @@ export const useSettingsMembersActions = (params: {
|
||||
headerItems.push({
|
||||
title: 'Upgrade to editor...',
|
||||
id: WorkspaceUserActionTypes.UpgradeEditor,
|
||||
disabled: statusIsExpired.value || statusIsCanceled.value,
|
||||
disabledTooltip: 'This workspace has an expired or canceled plan'
|
||||
disabled: statusIsCanceled.value,
|
||||
disabledTooltip: 'This workspace has a canceled plan'
|
||||
})
|
||||
}
|
||||
if (showDowngradeEditor.value) {
|
||||
@@ -256,13 +254,10 @@ export const useSettingsMembersActions = (params: {
|
||||
title: 'Downgrade to viewer...',
|
||||
id: WorkspaceUserActionTypes.DowngradeEditor,
|
||||
disabled:
|
||||
targetUserRole.value === Roles.Workspace.Admin ||
|
||||
statusIsExpired.value ||
|
||||
statusIsCanceled.value,
|
||||
disabledTooltip:
|
||||
statusIsExpired.value || statusIsCanceled.value
|
||||
? 'This workspace has an expired or canceled plan'
|
||||
: 'Admins must be on an Editor seat'
|
||||
targetUserRole.value === Roles.Workspace.Admin || statusIsCanceled.value,
|
||||
disabledTooltip: statusIsCanceled.value
|
||||
? 'This workspace has a canceled plan'
|
||||
: 'Admins must be on an Editor seat'
|
||||
})
|
||||
}
|
||||
// This will return post new workspace plan launch
|
||||
|
||||
@@ -2,7 +2,7 @@ import { graphql } from '~~/lib/common/generated/gql'
|
||||
import { workspacePlanQuery } from '~~/lib/workspaces/graphql/queries'
|
||||
import { useQuery } from '@vue/apollo-composable'
|
||||
import {
|
||||
PaidWorkspacePlansNew,
|
||||
PaidWorkspacePlans,
|
||||
UnpaidWorkspacePlans,
|
||||
WorkspacePlanBillingIntervals,
|
||||
isPaidPlan as isPaidPlanShared,
|
||||
@@ -69,8 +69,8 @@ export const useWorkspacePlan = (slug: string) => {
|
||||
const isFreePlan = computed(() => plan.value?.name === UnpaidWorkspacePlans.Free)
|
||||
const isBusinessPlan = computed(
|
||||
() =>
|
||||
plan.value?.name === PaidWorkspacePlansNew.Pro ||
|
||||
plan.value?.name === PaidWorkspacePlansNew.ProUnlimited
|
||||
plan.value?.name === PaidWorkspacePlans.Pro ||
|
||||
plan.value?.name === PaidWorkspacePlans.ProUnlimited
|
||||
)
|
||||
const isUnlimitedPlan = computed(
|
||||
() => plan.value?.name === UnpaidWorkspacePlans.Unlimited
|
||||
@@ -88,9 +88,6 @@ export const useWorkspacePlan = (slug: string) => {
|
||||
})
|
||||
|
||||
// Plan status information
|
||||
const statusIsExpired = computed(
|
||||
() => plan.value?.status === WorkspacePlanStatuses.Expired
|
||||
)
|
||||
const statusIsCanceled = computed(
|
||||
() => plan.value?.status === WorkspacePlanStatuses.Canceled
|
||||
)
|
||||
@@ -118,7 +115,7 @@ export const useWorkspacePlan = (slug: string) => {
|
||||
const editorSeatPriceFormatted = computed(() => {
|
||||
if (plan.value?.name && isPaidPlanShared(plan.value?.name)) {
|
||||
return formatPrice(
|
||||
prices.value?.[plan.value?.name as PaidWorkspacePlansNew]?.[
|
||||
prices.value?.[plan.value?.name as PaidWorkspacePlans]?.[
|
||||
intervalIsYearly.value
|
||||
? WorkspacePlanBillingIntervals.Yearly
|
||||
: WorkspacePlanBillingIntervals.Monthly
|
||||
@@ -134,7 +131,6 @@ export const useWorkspacePlan = (slug: string) => {
|
||||
|
||||
return {
|
||||
plan,
|
||||
statusIsExpired,
|
||||
statusIsCanceled,
|
||||
isFreePlan,
|
||||
billingInterval,
|
||||
|
||||
@@ -161,22 +161,6 @@ MULTI_REGION_CONFIG_PATH="multiregion.json"
|
||||
# STRIPE_API_KEY=sk_test_
|
||||
# STRIPE_ENDPOINT_SIGNING_KEY=whsec_
|
||||
#
|
||||
# WORKSPACE_STARTER_SEAT_STRIPE_PRODUCT_ID=prod_
|
||||
# WORKSPACE_MONTHLY_STARTER_SEAT_STRIPE_PRICE_ID=price_
|
||||
# WORKSPACE_YEARLY_STARTER_SEAT_STRIPE_PRICE_ID=price_
|
||||
#
|
||||
# WORKSPACE_GUEST_SEAT_STRIPE_PRODUCT_ID=prod_
|
||||
# WORKSPACE_MONTHLY_GUEST_SEAT_STRIPE_PRICE_ID=price_
|
||||
# WORKSPACE_YEARLY_GUEST_SEAT_STRIPE_PRICE_ID=price_
|
||||
#
|
||||
# WORKSPACE_PLUS_SEAT_STRIPE_PRODUCT_ID=prod_
|
||||
# WORKSPACE_MONTHLY_PLUS_SEAT_STRIPE_PRICE_ID=price_
|
||||
# WORKSPACE_YEARLY_PLUS_SEAT_STRIPE_PRICE_ID=price_
|
||||
#
|
||||
# WORKSPACE_BUSINESS_SEAT_STRIPE_PRODUCT_ID=prod_
|
||||
# WORKSPACE_MONTHLY_BUSINESS_SEAT_STRIPE_PRICE_ID=price_
|
||||
# WORKSPACE_YEARLY_BUSINESS_SEAT_STRIPE_PRICE_ID=price_
|
||||
#
|
||||
# WORKSPACE_TEAM_SEAT_STRIPE_PRODUCT_ID=
|
||||
# WORKSPACE_MONTHLY_TEAM_SEAT_STRIPE_PRICE_ID=
|
||||
# WORKSPACE_YEARLY_TEAM_SEAT_STRIPE_PRICE_ID=
|
||||
|
||||
@@ -13,23 +13,6 @@ RATELIMITER_ENABLED='false'
|
||||
|
||||
|
||||
#### Stripe product ids ####
|
||||
WORKSPACE_GUEST_SEAT_STRIPE_PRODUCT_ID='prod_RsMHWCX85KBTJd'
|
||||
WORKSPACE_MONTHLY_GUEST_SEAT_STRIPE_PRICE_ID='price_1QybYa7yKEDpA6qK7p7U3BTE'
|
||||
WORKSPACE_YEARLY_GUEST_SEAT_STRIPE_PRICE_ID='price_1QybZ77yKEDpA6qKqpBSV0OS'
|
||||
|
||||
WORKSPACE_STARTER_SEAT_STRIPE_PRODUCT_ID='prod_RsMItzuLAEKy50'
|
||||
WORKSPACE_MONTHLY_STARTER_SEAT_STRIPE_PRICE_ID='price_1QybZY7yKEDpA6qKl9TfgArD'
|
||||
WORKSPACE_YEARLY_STARTER_SEAT_STRIPE_PRICE_ID='price_1Qyba07yKEDpA6qKvH6iqDdU'
|
||||
|
||||
WORKSPACE_PLUS_SEAT_STRIPE_PRODUCT_ID='prod_RsMJxH2rfHbJRt'
|
||||
WORKSPACE_MONTHLY_PLUS_SEAT_STRIPE_PRICE_ID='price_1Qybaf7yKEDpA6qKHbS0lVXA'
|
||||
WORKSPACE_YEARLY_PLUS_SEAT_STRIPE_PRICE_ID='price_1Qybb97yKEDpA6qKEKXI0F2V'
|
||||
|
||||
WORKSPACE_BUSINESS_SEAT_STRIPE_PRODUCT_ID='prod_RsMKVRwEVBPl60'
|
||||
WORKSPACE_MONTHLY_BUSINESS_SEAT_STRIPE_PRICE_ID='price_1Qybbh7yKEDpA6qKqlN7fKUA'
|
||||
WORKSPACE_YEARLY_BUSINESS_SEAT_STRIPE_PRICE_ID='price_1Qybc67yKEDpA6qKtJ3DvClM'
|
||||
|
||||
#### NEW ####
|
||||
WORKSPACE_TEAM_SEAT_STRIPE_PRODUCT_ID='prod_RsMMdAkZIkAQRb'
|
||||
WORKSPACE_MONTHLY_TEAM_SEAT_GBP_STRIPE_PRICE_ID='price_1Qybdi7yKEDpA6qKlSaz9cFT'
|
||||
WORKSPACE_YEARLY_TEAM_SEAT_GBP_STRIPE_PRICE_ID='price_1R9qjc7yKEDpA6qKablYeaCe'
|
||||
|
||||
@@ -3,10 +3,6 @@ extend type WorkspaceMutations {
|
||||
}
|
||||
|
||||
enum PaidWorkspacePlans {
|
||||
starter
|
||||
plus
|
||||
business
|
||||
# New plans
|
||||
team
|
||||
teamUnlimited
|
||||
pro
|
||||
@@ -60,15 +56,8 @@ type WorkspaceBillingMutations {
|
||||
|
||||
enum WorkspacePlans {
|
||||
free
|
||||
starter
|
||||
plus
|
||||
business
|
||||
unlimited
|
||||
academia
|
||||
starterInvoiced
|
||||
plusInvoiced
|
||||
businessInvoiced
|
||||
# New plans
|
||||
team
|
||||
teamUnlimited
|
||||
teamUnlimitedInvoiced
|
||||
@@ -82,8 +71,6 @@ enum WorkspacePlanStatuses {
|
||||
paymentFailed
|
||||
cancelationScheduled
|
||||
canceled
|
||||
trial
|
||||
expired
|
||||
}
|
||||
|
||||
enum WorkspacePaymentMethod {
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { CommandModule } from 'yargs'
|
||||
import { cliLogger as logger } from '@/observability/logging'
|
||||
import { getWorkspaceBySlugOrIdFactory } from '@/modules/workspaces/repositories/workspaces'
|
||||
import {
|
||||
getWorkspaceBySlugOrIdFactory,
|
||||
getWorkspaceFactory
|
||||
} from '@/modules/workspaces/repositories/workspaces'
|
||||
import { db } from '@/db/knex'
|
||||
import { upsertPaidWorkspacePlanFactory } from '@/modules/gatekeeper/repositories/billing'
|
||||
import { upsertWorkspacePlanFactory } from '@/modules/gatekeeper/repositories/billing'
|
||||
import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace'
|
||||
import { PaidWorkspacePlans, PaidWorkspacePlanStatuses } from '@speckle/shared'
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { updateWorkspacePlanFactory } from '@/modules/gatekeeper/services/workspacePlans'
|
||||
|
||||
const command: CommandModule<
|
||||
unknown,
|
||||
@@ -24,21 +29,14 @@ const command: CommandModule<
|
||||
plan: {
|
||||
describe: 'Plan to set the status for',
|
||||
type: 'string',
|
||||
default: 'business',
|
||||
choices: ['business', 'starter', 'plus']
|
||||
default: PaidWorkspacePlans.Team,
|
||||
choices: [PaidWorkspacePlans.Team, PaidWorkspacePlans.Pro]
|
||||
},
|
||||
status: {
|
||||
describe: 'Status to set for the workspace plan',
|
||||
type: 'string',
|
||||
default: 'valid',
|
||||
choices: [
|
||||
'valid',
|
||||
'trial',
|
||||
'expired',
|
||||
'paymentFailed',
|
||||
'cancelationScheduled',
|
||||
'canceled'
|
||||
]
|
||||
choices: ['valid', 'paymentFailed', 'cancelationScheduled', 'canceled']
|
||||
}
|
||||
},
|
||||
handler: async (args) => {
|
||||
@@ -52,14 +50,17 @@ const command: CommandModule<
|
||||
)
|
||||
}
|
||||
|
||||
await upsertPaidWorkspacePlanFactory({ db })({
|
||||
workspacePlan: {
|
||||
createdAt: new Date(),
|
||||
workspaceId: workspace.id,
|
||||
name: args.plan,
|
||||
status: args.status
|
||||
}
|
||||
const updateWorkspacePlan = updateWorkspacePlanFactory({
|
||||
getWorkspace: getWorkspaceFactory({ db }),
|
||||
upsertWorkspacePlan: upsertWorkspacePlanFactory({ db }),
|
||||
emitEvent: getEventBus().emit
|
||||
})
|
||||
await updateWorkspacePlan({
|
||||
workspaceId: workspace.id,
|
||||
name: args.plan,
|
||||
status: args.status
|
||||
})
|
||||
|
||||
logger.info(`Plan set!`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1942,11 +1942,8 @@ export type OnboardingCompletionInput = {
|
||||
};
|
||||
|
||||
export const PaidWorkspacePlans = {
|
||||
Business: 'business',
|
||||
Plus: 'plus',
|
||||
Pro: 'pro',
|
||||
ProUnlimited: 'proUnlimited',
|
||||
Starter: 'starter',
|
||||
Team: 'team',
|
||||
TeamUnlimited: 'teamUnlimited'
|
||||
} as const;
|
||||
@@ -4901,9 +4898,7 @@ export type WorkspacePlanPrice = {
|
||||
export const WorkspacePlanStatuses = {
|
||||
CancelationScheduled: 'cancelationScheduled',
|
||||
Canceled: 'canceled',
|
||||
Expired: 'expired',
|
||||
PaymentFailed: 'paymentFailed',
|
||||
Trial: 'trial',
|
||||
Valid: 'valid'
|
||||
} as const;
|
||||
|
||||
@@ -4916,16 +4911,10 @@ export type WorkspacePlanUsage = {
|
||||
|
||||
export const WorkspacePlans = {
|
||||
Academia: 'academia',
|
||||
Business: 'business',
|
||||
BusinessInvoiced: 'businessInvoiced',
|
||||
Free: 'free',
|
||||
Plus: 'plus',
|
||||
PlusInvoiced: 'plusInvoiced',
|
||||
Pro: 'pro',
|
||||
ProUnlimited: 'proUnlimited',
|
||||
ProUnlimitedInvoiced: 'proUnlimitedInvoiced',
|
||||
Starter: 'starter',
|
||||
StarterInvoiced: 'starterInvoiced',
|
||||
Team: 'team',
|
||||
TeamUnlimited: 'teamUnlimited',
|
||||
TeamUnlimitedInvoiced: 'teamUnlimitedInvoiced',
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
import { ServerAcl, StreamAcl, UserEmails, Users, knex } from '@/modules/core/dbSchema'
|
||||
import { ServerAclRecord, UserRecord, UserWithRole } from '@/modules/core/helpers/types'
|
||||
import {
|
||||
ServerAcl,
|
||||
StreamAcl,
|
||||
Streams,
|
||||
UserEmails,
|
||||
Users,
|
||||
knex
|
||||
} from '@/modules/core/dbSchema'
|
||||
import {
|
||||
ServerAclRecord,
|
||||
StreamAclRecord,
|
||||
StreamRecord,
|
||||
UserRecord,
|
||||
UserWithRole
|
||||
} from '@/modules/core/helpers/types'
|
||||
import { Nullable } from '@/modules/shared/helpers/typeHelper'
|
||||
import { clamp, isArray, omit } from 'lodash'
|
||||
import { metaHelpers } from '@/modules/core/helpers/meta'
|
||||
@@ -36,11 +49,14 @@ import {
|
||||
UpdateUserServerRole
|
||||
} from '@/modules/core/domain/users/operations'
|
||||
import { removePrivateFields } from '@/modules/core/helpers/userHelper'
|
||||
import { WorkspaceAcl } from '@/modules/workspacesCore/helpers/db'
|
||||
export type { UserWithOptionalRole, GetUserParams }
|
||||
|
||||
const tables = {
|
||||
users: (db: Knex) => db<UserRecord>(Users.name),
|
||||
serverAcl: (db: Knex) => db<ServerAclRecord>(ServerAcl.name)
|
||||
serverAcl: (db: Knex) => db<ServerAclRecord>(ServerAcl.name),
|
||||
streamAcl: (db: Knex) => db<StreamAclRecord>(StreamAcl.name),
|
||||
streams: (db: Knex) => db<StreamRecord>(Streams.name)
|
||||
}
|
||||
|
||||
function sanitizeUserRecord<T extends Nullable<UserRecord>>(user: T): T {
|
||||
@@ -508,9 +524,32 @@ export const lookupUsersFactory =
|
||||
|
||||
// limit to given project
|
||||
if (projectId) {
|
||||
// Workspace implicit roles logic:
|
||||
// - User must have an explicit stream acl OR
|
||||
// - User must have a project workspace acl w/ non-guest role
|
||||
query
|
||||
.innerJoin(StreamAcl.name, StreamAcl.col.userId, Users.col.id)
|
||||
.andWhere(StreamAcl.col.resourceId, projectId)
|
||||
.innerJoin(Streams.name, (j1) => {
|
||||
j1.onVal(Streams.col.id, projectId)
|
||||
})
|
||||
.leftJoin(StreamAcl.name, (j1) => {
|
||||
j1.on(StreamAcl.col.resourceId, Streams.col.id).andOn(
|
||||
StreamAcl.col.userId,
|
||||
Users.col.id
|
||||
)
|
||||
})
|
||||
.leftJoin(WorkspaceAcl.name, (j1) => {
|
||||
j1.on(WorkspaceAcl.col.workspaceId, Streams.col.workspaceId).andOn(
|
||||
WorkspaceAcl.col.userId,
|
||||
Users.col.id
|
||||
)
|
||||
})
|
||||
.andWhere((w1) => {
|
||||
w1.whereNotNull(StreamAcl.col.role).orWhere(
|
||||
WorkspaceAcl.col.role,
|
||||
'!=',
|
||||
Roles.Workspace.Guest
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const rows = (await query) as UserRecord[]
|
||||
|
||||
@@ -38,6 +38,7 @@ import { getServerInfoFactory } from '@/modules/core/repositories/server'
|
||||
import { WorkspaceReadOnlyError } from '@/modules/gatekeeper/errors/billing'
|
||||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { PaidWorkspacePlanStatuses } from '@speckle/shared'
|
||||
|
||||
const getServerInfo = getServerInfoFactory({ db })
|
||||
const getUser = legacyGetUserFactory({ db })
|
||||
@@ -106,7 +107,7 @@ describe('Objects graphql @core', () => {
|
||||
|
||||
// Make the project read-only
|
||||
await db('workspace_plans')
|
||||
.update({ status: 'expired' })
|
||||
.update({ status: PaidWorkspacePlanStatuses.Canceled })
|
||||
.where({ workspaceId: workspace!.id })
|
||||
|
||||
const objectCreateRes = await apollo.execute(CreateObjectDocument, {
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
storeTokenResourceAccessDefinitionsFactory,
|
||||
storeTokenScopesFactory
|
||||
} from '@/modules/core/repositories/tokens'
|
||||
import { Scopes } from '@speckle/shared'
|
||||
import { PaidWorkspacePlans, Scopes } from '@speckle/shared'
|
||||
import { expect } from 'chai'
|
||||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
@@ -108,7 +108,7 @@ describe('Objects REST @core', () => {
|
||||
slug: ''
|
||||
}
|
||||
await createTestWorkspace(workspace, user, {
|
||||
addPlan: { name: 'business', status: 'expired' }
|
||||
addPlan: { name: PaidWorkspacePlans.Team, status: 'canceled' }
|
||||
})
|
||||
|
||||
const project = {
|
||||
|
||||
@@ -92,7 +92,7 @@ import {
|
||||
} from '@/test/speckle-helpers/regions'
|
||||
import { BasicTestStream, createTestStreams } from '@/test/speckle-helpers/streamHelper'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { Optional, Roles, Scopes, ServerScope } from '@speckle/shared'
|
||||
import { Optional, Roles, Scopes, ServerScope, WorkspacePlans } from '@speckle/shared'
|
||||
import { expect } from 'chai'
|
||||
|
||||
const validateStreamAccess = validateStreamAccessFactory({ authorizeResolver })
|
||||
@@ -239,10 +239,12 @@ describe('Core GraphQL Subscriptions (New)', () => {
|
||||
before(async () => {
|
||||
await Promise.all([
|
||||
createTestWorkspace(myMainWorkspace, me, {
|
||||
regionKey: isMultiRegion ? getMainTestRegionKey() : undefined
|
||||
regionKey: isMultiRegion ? getMainTestRegionKey() : undefined,
|
||||
addPlan: WorkspacePlans.Pro
|
||||
}),
|
||||
createTestWorkspace(otherGuysWorkspace, otherGuy, {
|
||||
regionKey: isMultiRegion ? getMainTestRegionKey() : undefined
|
||||
regionKey: isMultiRegion ? getMainTestRegionKey() : undefined,
|
||||
addPlan: WorkspacePlans.Pro
|
||||
})
|
||||
])
|
||||
|
||||
|
||||
@@ -1922,11 +1922,8 @@ export type OnboardingCompletionInput = {
|
||||
};
|
||||
|
||||
export const PaidWorkspacePlans = {
|
||||
Business: 'business',
|
||||
Plus: 'plus',
|
||||
Pro: 'pro',
|
||||
ProUnlimited: 'proUnlimited',
|
||||
Starter: 'starter',
|
||||
Team: 'team',
|
||||
TeamUnlimited: 'teamUnlimited'
|
||||
} as const;
|
||||
@@ -4881,9 +4878,7 @@ export type WorkspacePlanPrice = {
|
||||
export const WorkspacePlanStatuses = {
|
||||
CancelationScheduled: 'cancelationScheduled',
|
||||
Canceled: 'canceled',
|
||||
Expired: 'expired',
|
||||
PaymentFailed: 'paymentFailed',
|
||||
Trial: 'trial',
|
||||
Valid: 'valid'
|
||||
} as const;
|
||||
|
||||
@@ -4896,16 +4891,10 @@ export type WorkspacePlanUsage = {
|
||||
|
||||
export const WorkspacePlans = {
|
||||
Academia: 'academia',
|
||||
Business: 'business',
|
||||
BusinessInvoiced: 'businessInvoiced',
|
||||
Free: 'free',
|
||||
Plus: 'plus',
|
||||
PlusInvoiced: 'plusInvoiced',
|
||||
Pro: 'pro',
|
||||
ProUnlimited: 'proUnlimited',
|
||||
ProUnlimitedInvoiced: 'proUnlimitedInvoiced',
|
||||
Starter: 'starter',
|
||||
StarterInvoiced: 'starterInvoiced',
|
||||
Team: 'team',
|
||||
TeamUnlimited: 'teamUnlimited',
|
||||
TeamUnlimitedInvoiced: 'teamUnlimitedInvoiced',
|
||||
|
||||
@@ -13,9 +13,6 @@ import {
|
||||
Optional,
|
||||
PaidWorkspacePlan,
|
||||
PaidWorkspacePlans,
|
||||
PaidWorkspacePlansNew,
|
||||
PaidWorkspacePlansOld,
|
||||
TrialWorkspacePlan,
|
||||
UnpaidWorkspacePlan,
|
||||
WorkspacePlan,
|
||||
WorkspacePlanBillingIntervals
|
||||
@@ -42,10 +39,6 @@ export type GetWorkspaceWithPlan = (args: {
|
||||
workspaceId: string
|
||||
}) => Promise<Optional<Workspace & { plan: Nullable<WorkspacePlan> }>>
|
||||
|
||||
export type UpsertTrialWorkspacePlan = (args: {
|
||||
workspacePlan: TrialWorkspacePlan
|
||||
}) => Promise<void>
|
||||
|
||||
export type UpsertPaidWorkspacePlan = (args: {
|
||||
workspacePlan: PaidWorkspacePlan
|
||||
}) => Promise<void>
|
||||
@@ -131,7 +124,7 @@ export const SubscriptionData = z.object({
|
||||
status: z.union([
|
||||
z.literal('incomplete'),
|
||||
z.literal('incomplete_expired'),
|
||||
z.literal('trialing'),
|
||||
z.literal('trialing'), // TODO: Should we get rid of trial related states?
|
||||
z.literal('active'),
|
||||
z.literal('past_due'),
|
||||
z.literal('canceled'),
|
||||
@@ -145,20 +138,12 @@ export const SubscriptionData = z.object({
|
||||
export type SubscriptionData = z.infer<typeof SubscriptionData>
|
||||
|
||||
export const calculateSubscriptionSeats = ({
|
||||
subscriptionData,
|
||||
guestSeatProductId
|
||||
subscriptionData
|
||||
}: {
|
||||
subscriptionData: SubscriptionData
|
||||
guestSeatProductId: string
|
||||
}): { plan: number; guest: number } => {
|
||||
const guestProduct = subscriptionData.products.find(
|
||||
(p) => p.productId === guestSeatProductId
|
||||
)
|
||||
|
||||
const planProduct = subscriptionData.products.find(
|
||||
(p) => p.productId !== guestSeatProductId
|
||||
)
|
||||
return { guest: guestProduct?.quantity || 0, plan: planProduct?.quantity || 0 }
|
||||
}): number => {
|
||||
const product = subscriptionData.products[0]
|
||||
return product?.quantity || 0
|
||||
}
|
||||
|
||||
export type UpsertWorkspaceSubscription = (args: {
|
||||
@@ -189,16 +174,6 @@ export type GetWorkspacePlanProductId = (args: {
|
||||
workspacePlan: WorkspacePricingProducts
|
||||
}) => string
|
||||
|
||||
export type GbpOnlyPrice = { gbp: string }
|
||||
type GbpOnlyProductPrice = {
|
||||
monthly: GbpOnlyPrice
|
||||
yearly: GbpOnlyPrice
|
||||
}
|
||||
type OldProductPriceIds = Record<
|
||||
PaidWorkspacePlansOld | 'guest',
|
||||
{ productId: string } & GbpOnlyProductPrice
|
||||
>
|
||||
|
||||
export type MultiCurrencyPrice = {
|
||||
usd: string
|
||||
gbp: string
|
||||
@@ -208,20 +183,11 @@ type MultiCurrencyProductPrice = {
|
||||
yearly: MultiCurrencyPrice
|
||||
}
|
||||
|
||||
export const isMultiCurrencyPrice = (
|
||||
priceIds: GbpOnlyPrice | MultiCurrencyPrice
|
||||
): priceIds is MultiCurrencyPrice =>
|
||||
Object.values(Currency)
|
||||
.map((c) => c in priceIds)
|
||||
.every((p) => p === true)
|
||||
|
||||
type NewProductPriceIds = Record<
|
||||
PaidWorkspacePlansNew,
|
||||
export type WorkspacePlanProductAndPriceIds = Record<
|
||||
PaidWorkspacePlans,
|
||||
{ productId: string } & MultiCurrencyProductPrice
|
||||
>
|
||||
|
||||
export type WorkspacePlanProductAndPriceIds = OldProductPriceIds & NewProductPriceIds
|
||||
|
||||
export type GetWorkspacePlanProductAndPriceIds = () => WorkspacePlanProductAndPriceIds
|
||||
export type SubscriptionDataInput = OverrideProperties<
|
||||
SubscriptionData,
|
||||
|
||||
@@ -17,10 +17,6 @@ export type WorkspaceFeatureAccessFunction = (args: {
|
||||
workspaceId: string
|
||||
}) => Promise<boolean>
|
||||
|
||||
export type ChangeExpiredTrialWorkspacePlanStatuses = (args: {
|
||||
numberOfDays: number
|
||||
}) => Promise<WorkspacePlan[]>
|
||||
|
||||
export type GetWorkspacesByPlanDaysTillExpiry = (args: {
|
||||
daysTillExpiry: number
|
||||
planValidFor: number
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
getWorkspaceSubscriptionFactory
|
||||
} from '@/modules/gatekeeper/repositories/billing'
|
||||
import { countSeatsByTypeInWorkspaceFactory } from '@/modules/gatekeeper/repositories/workspaceSeat'
|
||||
import { addWorkspaceSubscriptionSeatIfNeededFactoryNew } from '@/modules/gatekeeper/services/subscriptions'
|
||||
import { addWorkspaceSubscriptionSeatIfNeededFactory } from '@/modules/gatekeeper/services/subscriptions'
|
||||
import {
|
||||
getWorkspacePlanPriceId,
|
||||
getWorkspacePlanProductId
|
||||
@@ -21,7 +21,7 @@ export const initializeEventListenersFactory =
|
||||
const quitCbs = [
|
||||
eventBus.listen(WorkspaceEvents.SeatUpdated, async ({ payload }) => {
|
||||
const addWorkspaceSubscriptionSeatIfNeeded =
|
||||
addWorkspaceSubscriptionSeatIfNeededFactoryNew({
|
||||
addWorkspaceSubscriptionSeatIfNeededFactory({
|
||||
getWorkspacePlan: getWorkspacePlanFactory({ db }),
|
||||
getWorkspaceSubscription: getWorkspaceSubscriptionFactory({ db }),
|
||||
getWorkspacePlanPriceId,
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { Resolvers } from '@/modules/core/graph/generated/graphql'
|
||||
import { authorizeResolver } from '@/modules/shared'
|
||||
import { Roles, throwUncoveredError } from '@speckle/shared'
|
||||
import {
|
||||
countWorkspaceRoleWithOptionalProjectRoleFactory,
|
||||
getWorkspaceFactory,
|
||||
getWorkspaceRoleForUserFactory,
|
||||
getWorkspacesProjectsCountsFactory
|
||||
@@ -38,15 +37,11 @@ import {
|
||||
} from '@/modules/gatekeeper/domain/billing'
|
||||
import { WorkspacePaymentMethod } from '@/test/graphql/generated/graphql'
|
||||
import { LogicError } from '@/modules/shared/errors'
|
||||
import { isNewPlanType } from '@/modules/gatekeeper/helpers/plans'
|
||||
import { getWorkspacePlanProductPricesFactory } from '@/modules/gatekeeper/services/prices'
|
||||
import { extendLoggerComponent } from '@/observability/logging'
|
||||
import { createCheckoutSessionFactory } from '@/modules/gatekeeper/clients/checkout/createCheckoutSession'
|
||||
import { startCheckoutSessionFactory } from '@/modules/gatekeeper/services/checkout/startCheckoutSession'
|
||||
import {
|
||||
upgradeWorkspaceSubscriptionFactoryNew,
|
||||
upgradeWorkspaceSubscriptionFactoryOld
|
||||
} from '@/modules/gatekeeper/services/subscriptions/upgradeWorkspaceSubscription'
|
||||
import { upgradeWorkspaceSubscriptionFactory } from '@/modules/gatekeeper/services/subscriptions/upgradeWorkspaceSubscription'
|
||||
import {
|
||||
countSeatsByTypeInWorkspaceFactory,
|
||||
createWorkspaceSeatFactory
|
||||
@@ -66,11 +61,6 @@ const { FF_GATEKEEPER_MODULE_ENABLED, FF_BILLING_INTEGRATION_ENABLED } =
|
||||
|
||||
const getWorkspacePlan = getWorkspacePlanFactory({ db })
|
||||
|
||||
async function shouldUseNewCheckoutFlow(workspaceId: string) {
|
||||
const workspacePlan = await getWorkspacePlan({ workspaceId })
|
||||
return workspacePlan && isNewPlanType(workspacePlan.name)
|
||||
}
|
||||
|
||||
export = FF_GATEKEEPER_MODULE_ENABLED
|
||||
? ({
|
||||
Workspace: {
|
||||
@@ -81,9 +71,6 @@ export = FF_GATEKEEPER_MODULE_ENABLED
|
||||
if (!workspacePlan) return null
|
||||
let paymentMethod: WorkspacePaymentMethod
|
||||
switch (workspacePlan.name) {
|
||||
case 'starter':
|
||||
case 'plus':
|
||||
case 'business':
|
||||
case 'team':
|
||||
case 'teamUnlimited':
|
||||
case 'pro':
|
||||
@@ -95,9 +82,6 @@ export = FF_GATEKEEPER_MODULE_ENABLED
|
||||
case 'free':
|
||||
paymentMethod = WorkspacePaymentMethod.Unpaid
|
||||
break
|
||||
case 'starterInvoiced':
|
||||
case 'plusInvoiced':
|
||||
case 'businessInvoiced':
|
||||
case 'proUnlimitedInvoiced':
|
||||
case 'teamUnlimitedInvoiced':
|
||||
paymentMethod = WorkspacePaymentMethod.Invoice
|
||||
@@ -244,13 +228,7 @@ export = FF_GATEKEEPER_MODULE_ENABLED
|
||||
switch (workspacePlan.name) {
|
||||
case 'unlimited':
|
||||
case 'academia':
|
||||
case 'business':
|
||||
case 'businessInvoiced':
|
||||
case 'free':
|
||||
case 'plus':
|
||||
case 'plusInvoiced':
|
||||
case 'starter':
|
||||
case 'starterInvoiced':
|
||||
case 'proUnlimitedInvoiced':
|
||||
case 'teamUnlimitedInvoiced':
|
||||
// not stripe paid plans and old plans do not have seats available
|
||||
@@ -397,9 +375,7 @@ export = FF_GATEKEEPER_MODULE_ENABLED
|
||||
Roles.Workspace.Admin,
|
||||
ctx.resourceAccessRules
|
||||
)
|
||||
const isNewFlow = await shouldUseNewCheckoutFlow(workspaceId)
|
||||
if (!isNewFlow)
|
||||
throw new Error('Checkout for old plans is not supported any more')
|
||||
|
||||
const createCheckoutSession = createCheckoutSessionFactory({
|
||||
stripe: getStripeClient(),
|
||||
frontendOrigin: getFrontendOrigin(),
|
||||
@@ -444,41 +420,22 @@ export = FF_GATEKEEPER_MODULE_ENABLED
|
||||
)
|
||||
const stripe = getStripeClient()
|
||||
|
||||
const currentPlan = await getWorkspacePlan({ workspaceId })
|
||||
const upgradeWorkspaceSubscription =
|
||||
currentPlan && isNewPlanType(currentPlan.name)
|
||||
? upgradeWorkspaceSubscriptionFactoryNew({
|
||||
getWorkspacePlan: getWorkspacePlanFactory({ db }),
|
||||
reconcileSubscriptionData: reconcileWorkspaceSubscriptionFactory({
|
||||
stripe
|
||||
}),
|
||||
countSeatsByTypeInWorkspace: countSeatsByTypeInWorkspaceFactory({
|
||||
db
|
||||
}),
|
||||
getWorkspaceSubscription: getWorkspaceSubscriptionFactory({ db }),
|
||||
getWorkspacePlanPriceId,
|
||||
getWorkspacePlanProductId,
|
||||
upsertWorkspacePlan: upsertPaidWorkspacePlanFactory({ db }),
|
||||
updateWorkspaceSubscription: upsertWorkspaceSubscriptionFactory({
|
||||
db
|
||||
})
|
||||
})
|
||||
: upgradeWorkspaceSubscriptionFactoryOld({
|
||||
getWorkspacePlan: getWorkspacePlanFactory({ db }),
|
||||
reconcileSubscriptionData: reconcileWorkspaceSubscriptionFactory({
|
||||
stripe
|
||||
}),
|
||||
countWorkspaceRole: countWorkspaceRoleWithOptionalProjectRoleFactory({
|
||||
db
|
||||
}),
|
||||
getWorkspaceSubscription: getWorkspaceSubscriptionFactory({ db }),
|
||||
getWorkspacePlanPriceId,
|
||||
getWorkspacePlanProductId,
|
||||
upsertWorkspacePlan: upsertPaidWorkspacePlanFactory({ db }),
|
||||
updateWorkspaceSubscription: upsertWorkspaceSubscriptionFactory({
|
||||
db
|
||||
})
|
||||
})
|
||||
const upgradeWorkspaceSubscription = upgradeWorkspaceSubscriptionFactory({
|
||||
getWorkspacePlan: getWorkspacePlanFactory({ db }),
|
||||
reconcileSubscriptionData: reconcileWorkspaceSubscriptionFactory({
|
||||
stripe
|
||||
}),
|
||||
countSeatsByTypeInWorkspace: countSeatsByTypeInWorkspaceFactory({
|
||||
db
|
||||
}),
|
||||
getWorkspaceSubscription: getWorkspaceSubscriptionFactory({ db }),
|
||||
getWorkspacePlanPriceId,
|
||||
getWorkspacePlanProductId,
|
||||
upsertWorkspacePlan: upsertPaidWorkspacePlanFactory({ db }),
|
||||
updateWorkspaceSubscription: upsertWorkspaceSubscriptionFactory({
|
||||
db
|
||||
})
|
||||
})
|
||||
await withOperationLogging(
|
||||
async () =>
|
||||
await upgradeWorkspaceSubscription({
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
import {
|
||||
isNewWorkspacePlan,
|
||||
PaidWorkspacePlansNew,
|
||||
PaidWorkspacePlansOld,
|
||||
WorkspacePlans
|
||||
} from '@speckle/shared'
|
||||
import { PaidWorkspacePlans, WorkspacePlans } from '@speckle/shared'
|
||||
|
||||
export const isNewPaidPlanType = (plan: WorkspacePlans): boolean => {
|
||||
return (Object.values(PaidWorkspacePlansNew) as string[]).includes(plan)
|
||||
}
|
||||
|
||||
export const isNewPlanType = (plan: WorkspacePlans): boolean => isNewWorkspacePlan(plan)
|
||||
|
||||
export const isOldPaidPlanType = (plan: WorkspacePlans): boolean => {
|
||||
return (Object.values(PaidWorkspacePlansOld) as string[]).includes(plan)
|
||||
export const isPaidPlanType = (plan: WorkspacePlans): boolean => {
|
||||
return (Object.values(PaidWorkspacePlans) as string[]).includes(plan)
|
||||
}
|
||||
|
||||
@@ -19,38 +19,23 @@ import {
|
||||
releaseTaskLockFactory
|
||||
} from '@/modules/core/repositories/scheduledTasks'
|
||||
import {
|
||||
changeExpiredTrialWorkspacePlanStatusesFactory,
|
||||
getWorkspacePlanByProjectIdFactory,
|
||||
getWorkspacePlanFactory,
|
||||
getWorkspacesByPlanAgeFactory,
|
||||
getWorkspaceSubscriptionsPastBillingCycleEndFactoryNewPlans,
|
||||
getWorkspaceSubscriptionsPastBillingCycleEndFactoryOldPlans,
|
||||
getWorkspaceSubscriptionsPastBillingCycleEndFactory,
|
||||
upsertWorkspaceSubscriptionFactory
|
||||
} from '@/modules/gatekeeper/repositories/billing'
|
||||
import {
|
||||
countWorkspaceRoleWithOptionalProjectRoleFactory,
|
||||
getWorkspaceCollaboratorsFactory
|
||||
} from '@/modules/workspaces/repositories/workspaces'
|
||||
import {
|
||||
getSubscriptionDataFactory,
|
||||
reconcileWorkspaceSubscriptionFactory
|
||||
} from '@/modules/gatekeeper/clients/stripe'
|
||||
import { ScheduleExecution } from '@/modules/core/domain/scheduledTasks/operations'
|
||||
import { EventBusEmit, getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { sendWorkspaceTrialExpiresEmailFactory } from '@/modules/gatekeeper/services/trialEmails'
|
||||
import { getServerInfoFactory } from '@/modules/core/repositories/server'
|
||||
import { findEmailsByUserIdFactory } from '@/modules/core/repositories/userEmails'
|
||||
import { sendEmail } from '@/modules/emails/services/sending'
|
||||
import { renderEmail } from '@/modules/emails/services/emailRendering'
|
||||
import coreModule from '@/modules/core/index'
|
||||
import { isProjectReadOnlyFactory } from '@/modules/gatekeeper/services/readOnly'
|
||||
import { WorkspaceReadOnlyError } from '@/modules/gatekeeper/errors/billing'
|
||||
import { InvalidLicenseError } from '@/modules/gatekeeper/errors/license'
|
||||
import {
|
||||
downscaleWorkspaceSubscriptionFactoryNew,
|
||||
downscaleWorkspaceSubscriptionFactoryOld,
|
||||
manageSubscriptionDownscaleFactoryNew,
|
||||
manageSubscriptionDownscaleFactoryOld
|
||||
downscaleWorkspaceSubscriptionFactory,
|
||||
manageSubscriptionDownscaleFactory
|
||||
} from '@/modules/gatekeeper/services/subscriptions/manageSubscriptionDownscale'
|
||||
import { countSeatsByTypeInWorkspaceFactory } from '@/modules/gatekeeper/repositories/workspaceSeat'
|
||||
|
||||
@@ -68,31 +53,16 @@ const scheduleWorkspaceSubscriptionDownscale = ({
|
||||
scheduleExecution: ScheduleExecution
|
||||
}) => {
|
||||
const stripe = getStripeClient()
|
||||
|
||||
const manageSubscriptionDownscaleOld = manageSubscriptionDownscaleFactoryOld({
|
||||
downscaleWorkspaceSubscription: downscaleWorkspaceSubscriptionFactoryOld({
|
||||
countWorkspaceRole: countWorkspaceRoleWithOptionalProjectRoleFactory({ db }),
|
||||
getWorkspacePlan: getWorkspacePlanFactory({ db }),
|
||||
reconcileSubscriptionData: reconcileWorkspaceSubscriptionFactory({ stripe }),
|
||||
getWorkspacePlanProductId
|
||||
}),
|
||||
getWorkspaceSubscriptions:
|
||||
getWorkspaceSubscriptionsPastBillingCycleEndFactoryOldPlans({
|
||||
db
|
||||
}),
|
||||
updateWorkspaceSubscription: upsertWorkspaceSubscriptionFactory({ db })
|
||||
})
|
||||
const manageSubscriptionDownscaleNew = manageSubscriptionDownscaleFactoryNew({
|
||||
downscaleWorkspaceSubscription: downscaleWorkspaceSubscriptionFactoryNew({
|
||||
const manageSubscriptionDownscale = manageSubscriptionDownscaleFactory({
|
||||
downscaleWorkspaceSubscription: downscaleWorkspaceSubscriptionFactory({
|
||||
countSeatsByTypeInWorkspace: countSeatsByTypeInWorkspaceFactory({ db }),
|
||||
getWorkspacePlan: getWorkspacePlanFactory({ db }),
|
||||
reconcileSubscriptionData: reconcileWorkspaceSubscriptionFactory({ stripe }),
|
||||
getWorkspacePlanProductId
|
||||
}),
|
||||
getWorkspaceSubscriptions:
|
||||
getWorkspaceSubscriptionsPastBillingCycleEndFactoryNewPlans({
|
||||
db
|
||||
}),
|
||||
getWorkspaceSubscriptions: getWorkspaceSubscriptionsPastBillingCycleEndFactory({
|
||||
db
|
||||
}),
|
||||
getSubscriptionData: getSubscriptionDataFactory({ stripe }),
|
||||
updateWorkspaceSubscription: upsertWorkspaceSubscriptionFactory({ db })
|
||||
})
|
||||
@@ -103,99 +73,12 @@ const scheduleWorkspaceSubscriptionDownscale = ({
|
||||
'WorkspaceSubscriptionDownscale',
|
||||
async (_scheduledTime, { logger }) => {
|
||||
await Promise.all([
|
||||
manageSubscriptionDownscaleOld({ logger }), // Only takes old plans subscriptions
|
||||
manageSubscriptionDownscaleNew({ logger }) // Only takes new plans subscriptions
|
||||
manageSubscriptionDownscale({ logger }) // Only takes new plans subscriptions
|
||||
])
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const scheduleWorkspaceTrialEmails = ({
|
||||
scheduleExecution
|
||||
}: {
|
||||
scheduleExecution: ScheduleExecution
|
||||
}) => {
|
||||
const sendWorkspaceTrialEmail = sendWorkspaceTrialExpiresEmailFactory({
|
||||
getServerInfo: getServerInfoFactory({ db }),
|
||||
getUserEmails: findEmailsByUserIdFactory({ db }),
|
||||
getWorkspaceCollaborators: getWorkspaceCollaboratorsFactory({ db }),
|
||||
sendEmail,
|
||||
renderEmail
|
||||
})
|
||||
// TODO: make this a daily thing
|
||||
// const cronExpression = '*/5 * * * * *'
|
||||
// every day at noon
|
||||
const cronExpression = '0 12 * * *'
|
||||
return scheduleExecution(
|
||||
cronExpression,
|
||||
'WorkspaceTrialEmails',
|
||||
async (_scheduledTime, { logger }) => {
|
||||
logger.info('Sending workspace trial emails.')
|
||||
const getWorkspacesByPlanAge = getWorkspacesByPlanAgeFactory({ db })
|
||||
const trialValidForDays = 31
|
||||
const trialWorkspacesExpireIn3Days = await getWorkspacesByPlanAge({
|
||||
daysTillExpiry: 3,
|
||||
planValidFor: trialValidForDays,
|
||||
plan: 'starter',
|
||||
status: 'trial'
|
||||
})
|
||||
if (trialWorkspacesExpireIn3Days.length) {
|
||||
await Promise.all(
|
||||
trialWorkspacesExpireIn3Days.map((workspace) =>
|
||||
sendWorkspaceTrialEmail({ workspace, expiresInDays: 3 })
|
||||
)
|
||||
)
|
||||
}
|
||||
const trialWorkspacesExpireToday = await getWorkspacesByPlanAge({
|
||||
daysTillExpiry: 0,
|
||||
planValidFor: trialValidForDays,
|
||||
plan: 'starter',
|
||||
status: 'trial'
|
||||
})
|
||||
if (trialWorkspacesExpireToday.length) {
|
||||
await Promise.all(
|
||||
trialWorkspacesExpireToday.map((workspace) =>
|
||||
sendWorkspaceTrialEmail({ workspace, expiresInDays: 0 })
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const scheduleWorkspaceTrialExpiry = ({
|
||||
scheduleExecution,
|
||||
emit
|
||||
}: {
|
||||
scheduleExecution: ScheduleExecution
|
||||
emit: EventBusEmit
|
||||
}) => {
|
||||
const changeExpiredStatuses = changeExpiredTrialWorkspacePlanStatusesFactory({ db })
|
||||
const cronExpression = '*/5 * * * *'
|
||||
return scheduleExecution(
|
||||
cronExpression,
|
||||
'WorkspaceTrialExpiry',
|
||||
async (_scheduledTime, { logger }) => {
|
||||
const expiredWorkspacePlans = await changeExpiredStatuses({ numberOfDays: 31 })
|
||||
|
||||
if (expiredWorkspacePlans.length) {
|
||||
logger.info(
|
||||
{ workspaceIds: expiredWorkspacePlans.map((p) => p.workspaceId) },
|
||||
'Workspace trial expired for {workspaceIds}.'
|
||||
)
|
||||
await Promise.all(
|
||||
expiredWorkspacePlans.map(async (plan) =>
|
||||
emit({
|
||||
eventName: 'gatekeeper.workspace-trial-expired',
|
||||
payload: { workspaceId: plan.workspaceId }
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
let scheduledTasks: cron.ScheduledTask[] = []
|
||||
let quitListeners: (() => void) | undefined = undefined
|
||||
|
||||
@@ -221,18 +104,12 @@ const gatekeeperModule: SpeckleModule = {
|
||||
getWorkspacePlanProductAndPriceIds()
|
||||
app.use(getBillingRouter())
|
||||
|
||||
const eventBus = getEventBus()
|
||||
|
||||
const scheduleExecution = scheduleExecutionFactory({
|
||||
acquireTaskLock: acquireTaskLockFactory({ db }),
|
||||
releaseTaskLock: releaseTaskLockFactory({ db })
|
||||
})
|
||||
|
||||
scheduledTasks = [
|
||||
scheduleWorkspaceSubscriptionDownscale({ scheduleExecution }),
|
||||
scheduleWorkspaceTrialEmails({ scheduleExecution }),
|
||||
scheduleWorkspaceTrialExpiry({ scheduleExecution, emit: eventBus.emit })
|
||||
]
|
||||
scheduledTasks = [scheduleWorkspaceSubscriptionDownscale({ scheduleExecution })]
|
||||
|
||||
quitListeners = initializeEventListenersFactory({
|
||||
db,
|
||||
|
||||
@@ -14,24 +14,18 @@ import {
|
||||
GetWorkspaceSubscription,
|
||||
GetWorkspaceSubscriptionBySubscriptionId,
|
||||
GetWorkspaceSubscriptions,
|
||||
UpsertTrialWorkspacePlan,
|
||||
UpsertUnpaidWorkspacePlan,
|
||||
GetWorkspaceWithPlan,
|
||||
GetWorkspacePlansByWorkspaceId
|
||||
} from '@/modules/gatekeeper/domain/billing'
|
||||
import {
|
||||
ChangeExpiredTrialWorkspacePlanStatuses,
|
||||
GetWorkspacesByPlanDaysTillExpiry,
|
||||
GetWorkspacePlanByProjectId
|
||||
} from '@/modules/gatekeeper/domain/operations'
|
||||
import { formatJsonArrayRecords } from '@/modules/shared/helpers/dbHelper'
|
||||
import { Workspace } from '@/modules/workspacesCore/domain/types'
|
||||
import { Workspaces } from '@/modules/workspacesCore/helpers/db'
|
||||
import {
|
||||
PaidWorkspacePlansNew,
|
||||
PaidWorkspacePlansOld,
|
||||
WorkspacePlan
|
||||
} from '@speckle/shared'
|
||||
import { PaidWorkspacePlans, WorkspacePlan } from '@speckle/shared'
|
||||
import { Knex } from 'knex'
|
||||
import { omit } from 'lodash'
|
||||
|
||||
@@ -122,29 +116,12 @@ export const upsertPaidWorkspacePlanFactory = ({
|
||||
db: Knex
|
||||
}): UpsertPaidWorkspacePlan => upsertWorkspacePlanFactory({ db })
|
||||
|
||||
export const upsertTrialWorkspacePlanFactory = ({
|
||||
db
|
||||
}: {
|
||||
db: Knex
|
||||
}): UpsertTrialWorkspacePlan => upsertWorkspacePlanFactory({ db })
|
||||
|
||||
export const upsertUnpaidWorkspacePlanFactory = ({
|
||||
db
|
||||
}: {
|
||||
db: Knex
|
||||
}): UpsertUnpaidWorkspacePlan => upsertWorkspacePlanFactory({ db })
|
||||
|
||||
export const changeExpiredTrialWorkspacePlanStatusesFactory =
|
||||
({ db }: { db: Knex }): ChangeExpiredTrialWorkspacePlanStatuses =>
|
||||
async ({ numberOfDays }) => {
|
||||
return await tables
|
||||
.workspacePlans(db)
|
||||
.where({ status: 'trial' })
|
||||
.andWhereRaw(`"createdAt" + make_interval(days => ${numberOfDays}) < now()`)
|
||||
.update({ status: 'expired' })
|
||||
.returning('*')
|
||||
}
|
||||
|
||||
export const getWorkspacesByPlanAgeFactory =
|
||||
({ db }: { db: Knex }): GetWorkspacesByPlanDaysTillExpiry =>
|
||||
async ({ daysTillExpiry, planValidFor, plan, status }) => {
|
||||
@@ -235,10 +212,7 @@ export const getWorkspaceSubscriptionBySubscriptionIdFactory =
|
||||
return subscription ?? null
|
||||
}
|
||||
|
||||
const newPlans = Object.values(PaidWorkspacePlansNew)
|
||||
const oldPlans = Object.values(PaidWorkspacePlansOld)
|
||||
|
||||
export const getWorkspaceSubscriptionsPastBillingCycleEndFactoryOldPlans =
|
||||
export const getWorkspaceSubscriptionsPastBillingCycleEndFactory =
|
||||
({ db }: { db: Knex }): GetWorkspaceSubscriptions =>
|
||||
async () => {
|
||||
const cycleEnd = new Date()
|
||||
@@ -250,24 +224,7 @@ export const getWorkspaceSubscriptionsPastBillingCycleEndFactoryOldPlans =
|
||||
WorkspacePlans.col.workspaceId,
|
||||
'workspace_subscriptions.workspaceId'
|
||||
)
|
||||
.whereIn(WorkspacePlans.col.name, oldPlans)
|
||||
.where('currentBillingCycleEnd', '<', cycleEnd)
|
||||
.select(WorkspaceSubscriptions.cols)
|
||||
}
|
||||
|
||||
export const getWorkspaceSubscriptionsPastBillingCycleEndFactoryNewPlans =
|
||||
({ db }: { db: Knex }): GetWorkspaceSubscriptions =>
|
||||
async () => {
|
||||
const cycleEnd = new Date()
|
||||
cycleEnd.setMinutes(cycleEnd.getMinutes() + 5)
|
||||
return await tables
|
||||
.workspaceSubscriptions(db)
|
||||
.join(
|
||||
WorkspacePlans.name,
|
||||
WorkspacePlans.col.workspaceId,
|
||||
'workspace_subscriptions.workspaceId'
|
||||
)
|
||||
.whereIn(WorkspacePlans.col.name, newPlans)
|
||||
.whereIn(WorkspacePlans.col.name, Object.values(PaidWorkspacePlans))
|
||||
.where('currentBillingCycleEnd', '<', cycleEnd)
|
||||
.select(WorkspaceSubscriptions.cols)
|
||||
}
|
||||
|
||||
@@ -97,13 +97,6 @@ export const startCheckoutSessionFactory =
|
||||
checkoutSessionId: existingCheckoutSession?.id
|
||||
})
|
||||
break
|
||||
|
||||
// maybe, we can reactivate canceled plans via the sub in stripe, but this is fine too
|
||||
// it will create a new customer and a new sub though, the reactivation would use the existing customer
|
||||
case 'trial':
|
||||
case 'expired':
|
||||
// lets go ahead and pay
|
||||
break
|
||||
default:
|
||||
throwUncoveredError(existingWorkspacePlan)
|
||||
}
|
||||
|
||||
@@ -16,11 +16,9 @@ export const canWorkspaceAccessFeatureFactory =
|
||||
if (!workspacePlan) return false
|
||||
switch (workspacePlan.status) {
|
||||
case 'valid':
|
||||
case 'trial':
|
||||
case 'paymentFailed':
|
||||
case 'cancelationScheduled':
|
||||
break
|
||||
case 'expired':
|
||||
case 'canceled':
|
||||
return false
|
||||
default:
|
||||
|
||||
@@ -5,37 +5,12 @@ import {
|
||||
import { Currency } from '@/modules/gatekeeperCore/domain/billing'
|
||||
import { expectToThrow } from '@/test/assertionHelper'
|
||||
import { mockRedisCacheProviderFactory } from '@/test/redisHelper'
|
||||
import {
|
||||
PaidWorkspacePlans,
|
||||
PaidWorkspacePlansNew,
|
||||
WorkspaceGuestSeatType,
|
||||
WorkspacePlanBillingIntervals
|
||||
} from '@speckle/shared'
|
||||
import { PaidWorkspacePlans, WorkspacePlanBillingIntervals } from '@speckle/shared'
|
||||
import { expect } from 'chai'
|
||||
import { flatten } from 'lodash'
|
||||
import { WorkspacePlanProductAndPriceIds } from '@/modules/gatekeeper/domain/billing'
|
||||
|
||||
const testProductAndPriceIds: WorkspacePlanProductAndPriceIds = {
|
||||
[WorkspaceGuestSeatType]: {
|
||||
productId: 'prod_guest',
|
||||
monthly: { gbp: 'price_guest_monthly_gbp' },
|
||||
yearly: { gbp: 'price_guest_yearly_gbp' }
|
||||
},
|
||||
[PaidWorkspacePlans.Starter]: {
|
||||
productId: 'prod_starter',
|
||||
monthly: { gbp: 'price_starter_monthly_gbp' },
|
||||
yearly: { gbp: 'price_starter_yearly_gbp' }
|
||||
},
|
||||
[PaidWorkspacePlans.Plus]: {
|
||||
productId: 'prod_plus',
|
||||
monthly: { gbp: 'price_plus_monthly_gbp' },
|
||||
yearly: { gbp: 'price_plus_yearly_gbp' }
|
||||
},
|
||||
[PaidWorkspacePlans.Business]: {
|
||||
productId: 'prod_business',
|
||||
monthly: { gbp: 'price_business_monthly_gbp' },
|
||||
yearly: { gbp: 'price_business_yearly_gbp' }
|
||||
},
|
||||
[PaidWorkspacePlans.Team]: {
|
||||
productId: 'prod_team',
|
||||
monthly: { gbp: 'price_team_monthly_gbp', usd: 'price_team_monthly_usd' },
|
||||
@@ -71,7 +46,7 @@ const testProductAndPriceIds: WorkspacePlanProductAndPriceIds = {
|
||||
}
|
||||
|
||||
const fakeGetRecurringPrices = async () => {
|
||||
const pricePairs = Object.values(PaidWorkspacePlansNew).map((plan) => {
|
||||
const pricePairs = Object.values(PaidWorkspacePlans).map((plan) => {
|
||||
const { productId, monthly, yearly } = testProductAndPriceIds[plan]
|
||||
return [
|
||||
{
|
||||
@@ -116,7 +91,7 @@ describe('prices @gatekeeper', () => {
|
||||
expect(result).to.be.ok
|
||||
for (const currency of Object.values(Currency)) {
|
||||
const newPlans = result[currency]
|
||||
for (const newPaidPlan of Object.values(PaidWorkspacePlansNew)) {
|
||||
for (const newPaidPlan of Object.values(PaidWorkspacePlans)) {
|
||||
const plan = newPlans[newPaidPlan]
|
||||
for (const interval of Object.values(WorkspacePlanBillingIntervals)) {
|
||||
const price = plan[interval]
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
wrapFactoryWithCache
|
||||
} from '@/modules/shared/utils/caching'
|
||||
import {
|
||||
PaidWorkspacePlansNew,
|
||||
PaidWorkspacePlans,
|
||||
TIME_MS,
|
||||
WorkspacePlanBillingIntervals
|
||||
} from '@speckle/shared'
|
||||
@@ -29,7 +29,7 @@ export const getFreshWorkspacePlanProductPricesFactory =
|
||||
const productAndPriceIds = deps.getWorkspacePlanProductAndPriceIds()
|
||||
|
||||
const productPrices = Object.values(Currency).reduce((acc, currency) => {
|
||||
const currencyPrices = Object.values(PaidWorkspacePlansNew).reduce(
|
||||
const currencyPrices = Object.values(PaidWorkspacePlans).reduce(
|
||||
(acc, paidPlan) => {
|
||||
const intervalPrices = Object.values(WorkspacePlanBillingIntervals).reduce(
|
||||
(acc, interval) => {
|
||||
|
||||
@@ -16,9 +16,8 @@ import {
|
||||
WorkspacePlanNotFoundError,
|
||||
WorkspaceSubscriptionNotFoundError
|
||||
} from '@/modules/gatekeeper/errors/billing'
|
||||
import { isNewPlanType } from '@/modules/gatekeeper/helpers/plans'
|
||||
import {
|
||||
PaidWorkspacePlansNew,
|
||||
PaidWorkspacePlans,
|
||||
PaidWorkspacePlanStatuses,
|
||||
throwUncoveredError
|
||||
} from '@speckle/shared'
|
||||
@@ -70,9 +69,6 @@ export const handleSubscriptionUpdateFactory =
|
||||
|
||||
if (status) {
|
||||
switch (workspacePlan.name) {
|
||||
case 'starter':
|
||||
case 'plus':
|
||||
case 'business':
|
||||
case 'team':
|
||||
case 'teamUnlimited':
|
||||
case 'pro':
|
||||
@@ -80,9 +76,6 @@ export const handleSubscriptionUpdateFactory =
|
||||
break
|
||||
case 'unlimited':
|
||||
case 'academia':
|
||||
case 'starterInvoiced':
|
||||
case 'plusInvoiced':
|
||||
case 'businessInvoiced':
|
||||
case 'proUnlimitedInvoiced':
|
||||
case 'teamUnlimitedInvoiced':
|
||||
case 'free':
|
||||
@@ -105,7 +98,7 @@ export const handleSubscriptionUpdateFactory =
|
||||
}
|
||||
}
|
||||
|
||||
export const addWorkspaceSubscriptionSeatIfNeededFactoryNew =
|
||||
export const addWorkspaceSubscriptionSeatIfNeededFactory =
|
||||
({
|
||||
getWorkspacePlan,
|
||||
getWorkspaceSubscription,
|
||||
@@ -134,11 +127,6 @@ export const addWorkspaceSubscriptionSeatIfNeededFactoryNew =
|
||||
const workspaceSubscription = await getWorkspaceSubscription({ workspaceId })
|
||||
if (!workspaceSubscription) return
|
||||
// if (!workspaceSubscription) throw new WorkspaceSubscriptionNotFoundError()
|
||||
const isNewPlan = isNewPlanType(workspacePlan.name)
|
||||
if (!isNewPlan) {
|
||||
// old plans not supported
|
||||
return
|
||||
}
|
||||
|
||||
switch (workspacePlan.name) {
|
||||
case 'team':
|
||||
@@ -146,16 +134,13 @@ export const addWorkspaceSubscriptionSeatIfNeededFactoryNew =
|
||||
case 'pro':
|
||||
case 'proUnlimited':
|
||||
// If viewer seat type, we don't need to do anything
|
||||
if (seatType === WorkspaceSeatType.Viewer) return
|
||||
case 'starter':
|
||||
case 'plus':
|
||||
case 'business':
|
||||
break
|
||||
if (seatType === WorkspaceSeatType.Viewer) {
|
||||
return
|
||||
} else {
|
||||
break
|
||||
}
|
||||
case 'unlimited':
|
||||
case 'academia':
|
||||
case 'starterInvoiced':
|
||||
case 'plusInvoiced':
|
||||
case 'businessInvoiced':
|
||||
case 'proUnlimitedInvoiced':
|
||||
case 'teamUnlimitedInvoiced':
|
||||
case 'free':
|
||||
@@ -208,7 +193,7 @@ export const getTotalSeatsCountByPlanFactory =
|
||||
workspacePlan,
|
||||
subscriptionData
|
||||
}: {
|
||||
workspacePlan: PaidWorkspacePlansNew
|
||||
workspacePlan: PaidWorkspacePlans
|
||||
subscriptionData: Pick<SubscriptionData, 'products'>
|
||||
}) => {
|
||||
const productId = getWorkspacePlanProductId({
|
||||
|
||||
+2
-126
@@ -12,10 +12,7 @@ import {
|
||||
WorkspacePlanMismatchError,
|
||||
WorkspacePlanNotFoundError
|
||||
} from '@/modules/gatekeeper/errors/billing'
|
||||
import { calculateNewBillingCycleEnd } from '@/modules/gatekeeper/services/subscriptions/calculateNewBillingCycleEnd'
|
||||
import { mutateSubscriptionDataWithNewValidSeatNumbers } from '@/modules/gatekeeper/services/subscriptions/mutateSubscriptionDataWithNewValidSeatNumbers'
|
||||
import { NotImplementedError } from '@/modules/shared/errors'
|
||||
import { CountWorkspaceRoleWithOptionalProjectRole } from '@/modules/workspaces/domain/operations'
|
||||
import { Logger } from '@/observability/logging'
|
||||
import { throwUncoveredError } from '@speckle/shared'
|
||||
import { cloneDeep, isEqual } from 'lodash'
|
||||
@@ -24,80 +21,7 @@ type DownscaleWorkspaceSubscription = (args: {
|
||||
workspaceSubscription: WorkspaceSubscription
|
||||
}) => Promise<boolean>
|
||||
|
||||
export const downscaleWorkspaceSubscriptionFactoryOld =
|
||||
({
|
||||
getWorkspacePlan,
|
||||
countWorkspaceRole,
|
||||
getWorkspacePlanProductId,
|
||||
reconcileSubscriptionData
|
||||
}: {
|
||||
getWorkspacePlan: GetWorkspacePlan
|
||||
countWorkspaceRole: CountWorkspaceRoleWithOptionalProjectRole
|
||||
getWorkspacePlanProductId: GetWorkspacePlanProductId
|
||||
reconcileSubscriptionData: ReconcileSubscriptionData
|
||||
}): DownscaleWorkspaceSubscription =>
|
||||
async ({ workspaceSubscription }) => {
|
||||
const workspaceId = workspaceSubscription.workspaceId
|
||||
|
||||
const workspacePlan = await getWorkspacePlan({ workspaceId })
|
||||
if (!workspacePlan) throw new WorkspacePlanNotFoundError()
|
||||
|
||||
switch (workspacePlan.name) {
|
||||
case 'team':
|
||||
case 'teamUnlimited':
|
||||
case 'pro':
|
||||
case 'proUnlimited':
|
||||
case 'proUnlimitedInvoiced':
|
||||
case 'teamUnlimitedInvoiced':
|
||||
// Cause seat types matter, a future issue
|
||||
throw new NotImplementedError()
|
||||
case 'starter':
|
||||
case 'plus':
|
||||
case 'business':
|
||||
break
|
||||
case 'unlimited':
|
||||
case 'academia':
|
||||
case 'starterInvoiced':
|
||||
case 'plusInvoiced':
|
||||
case 'businessInvoiced':
|
||||
case 'free':
|
||||
throw new WorkspacePlanMismatchError()
|
||||
default:
|
||||
throwUncoveredError(workspacePlan)
|
||||
}
|
||||
|
||||
if (workspacePlan.status === 'canceled') return false
|
||||
|
||||
// TODO: Guests will be able to have a paid seat
|
||||
const [guestCount, memberCount, adminCount] = await Promise.all([
|
||||
countWorkspaceRole({ workspaceId, workspaceRole: 'workspace:guest' }),
|
||||
countWorkspaceRole({ workspaceId, workspaceRole: 'workspace:member' }),
|
||||
countWorkspaceRole({ workspaceId, workspaceRole: 'workspace:admin' })
|
||||
])
|
||||
|
||||
const subscriptionData = cloneDeep(workspaceSubscription.subscriptionData)
|
||||
|
||||
mutateSubscriptionDataWithNewValidSeatNumbers({
|
||||
seatCount: guestCount,
|
||||
workspacePlan: 'guest',
|
||||
getWorkspacePlanProductId,
|
||||
subscriptionData
|
||||
})
|
||||
mutateSubscriptionDataWithNewValidSeatNumbers({
|
||||
seatCount: memberCount + adminCount,
|
||||
workspacePlan: workspacePlan.name,
|
||||
getWorkspacePlanProductId,
|
||||
subscriptionData
|
||||
})
|
||||
|
||||
if (!isEqual(subscriptionData, workspaceSubscription.subscriptionData)) {
|
||||
await reconcileSubscriptionData({ subscriptionData, prorationBehavior: 'none' })
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const downscaleWorkspaceSubscriptionFactoryNew =
|
||||
export const downscaleWorkspaceSubscriptionFactory =
|
||||
({
|
||||
getWorkspacePlan,
|
||||
countSeatsByTypeInWorkspace,
|
||||
@@ -121,14 +45,8 @@ export const downscaleWorkspaceSubscriptionFactoryNew =
|
||||
case 'pro':
|
||||
case 'proUnlimited':
|
||||
break
|
||||
case 'starter':
|
||||
case 'plus':
|
||||
case 'business':
|
||||
case 'unlimited':
|
||||
case 'academia':
|
||||
case 'starterInvoiced':
|
||||
case 'plusInvoiced':
|
||||
case 'businessInvoiced':
|
||||
case 'proUnlimitedInvoiced':
|
||||
case 'teamUnlimitedInvoiced':
|
||||
case 'free':
|
||||
@@ -160,48 +78,7 @@ export const downscaleWorkspaceSubscriptionFactoryNew =
|
||||
return false
|
||||
}
|
||||
|
||||
export const manageSubscriptionDownscaleFactoryOld =
|
||||
({
|
||||
getWorkspaceSubscriptions,
|
||||
downscaleWorkspaceSubscription,
|
||||
updateWorkspaceSubscription
|
||||
}: {
|
||||
getWorkspaceSubscriptions: GetWorkspaceSubscriptions
|
||||
downscaleWorkspaceSubscription: DownscaleWorkspaceSubscription
|
||||
updateWorkspaceSubscription: UpsertWorkspaceSubscription
|
||||
}) =>
|
||||
async (context: { logger: Logger }) => {
|
||||
const { logger } = context
|
||||
const subscriptions = await getWorkspaceSubscriptions()
|
||||
for (const workspaceSubscription of subscriptions) {
|
||||
const log = logger.child({ workspaceId: workspaceSubscription.workspaceId })
|
||||
try {
|
||||
const subDownscaled = await downscaleWorkspaceSubscription({
|
||||
workspaceSubscription
|
||||
})
|
||||
if (subDownscaled) {
|
||||
log.info(
|
||||
'Downscaled workspace subscription to match the current workspace team'
|
||||
)
|
||||
} else {
|
||||
log.info('Did not need to downscale the workspace subscription')
|
||||
}
|
||||
} catch (err) {
|
||||
log.error({ err }, 'Failed to downscale workspace subscription')
|
||||
}
|
||||
const newBillingCycleEnd = calculateNewBillingCycleEnd({ workspaceSubscription })
|
||||
const updatedWorkspaceSubscription = {
|
||||
...workspaceSubscription,
|
||||
currentBillingCycleEnd: newBillingCycleEnd
|
||||
}
|
||||
await updateWorkspaceSubscription({
|
||||
workspaceSubscription: updatedWorkspaceSubscription
|
||||
})
|
||||
log.info({ updatedWorkspaceSubscription }, 'Updated workspace billing cycle end')
|
||||
}
|
||||
}
|
||||
|
||||
export const manageSubscriptionDownscaleFactoryNew =
|
||||
export const manageSubscriptionDownscaleFactory =
|
||||
({
|
||||
getWorkspaceSubscriptions,
|
||||
downscaleWorkspaceSubscription,
|
||||
@@ -219,7 +96,6 @@ export const manageSubscriptionDownscaleFactoryNew =
|
||||
for (const workspaceSubscription of subscriptions) {
|
||||
const log = logger.child({ workspaceId: workspaceSubscription.workspaceId })
|
||||
try {
|
||||
//TODO:
|
||||
const subDownscaled = await downscaleWorkspaceSubscription({
|
||||
workspaceSubscription
|
||||
})
|
||||
|
||||
+6
-230
@@ -18,233 +18,18 @@ import {
|
||||
WorkspacePlanUpgradeError,
|
||||
WorkspaceSubscriptionNotFoundError
|
||||
} from '@/modules/gatekeeper/errors/billing'
|
||||
import {
|
||||
isNewPaidPlanType,
|
||||
isNewPlanType,
|
||||
isOldPaidPlanType
|
||||
} from '@/modules/gatekeeper/helpers/plans'
|
||||
import { isPaidPlanType } from '@/modules/gatekeeper/helpers/plans'
|
||||
import { calculateNewBillingCycleEnd } from '@/modules/gatekeeper/services/subscriptions/calculateNewBillingCycleEnd'
|
||||
import { mutateSubscriptionDataWithNewValidSeatNumbers } from '@/modules/gatekeeper/services/subscriptions/mutateSubscriptionDataWithNewValidSeatNumbers'
|
||||
import { isUpgradeWorkspacePlanValid } from '@/modules/gatekeeper/services/upgrades'
|
||||
import { NotImplementedError } from '@/modules/shared/errors'
|
||||
import { CountWorkspaceRoleWithOptionalProjectRole } from '@/modules/workspaces/domain/operations'
|
||||
import {
|
||||
PaidWorkspacePlans,
|
||||
PaidWorkspacePlansNew,
|
||||
throwUncoveredError,
|
||||
WorkspacePlanBillingIntervals,
|
||||
xor
|
||||
WorkspacePlanBillingIntervals
|
||||
} from '@speckle/shared'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
export const upgradeWorkspaceSubscriptionFactoryOld =
|
||||
({
|
||||
getWorkspacePlan,
|
||||
getWorkspacePlanProductId,
|
||||
getWorkspacePlanPriceId,
|
||||
getWorkspaceSubscription,
|
||||
reconcileSubscriptionData,
|
||||
updateWorkspaceSubscription,
|
||||
countWorkspaceRole,
|
||||
upsertWorkspacePlan
|
||||
}: {
|
||||
getWorkspacePlan: GetWorkspacePlan
|
||||
getWorkspacePlanProductId: GetWorkspacePlanProductId
|
||||
getWorkspacePlanPriceId: GetWorkspacePlanPriceId
|
||||
getWorkspaceSubscription: GetWorkspaceSubscription
|
||||
reconcileSubscriptionData: ReconcileSubscriptionData
|
||||
updateWorkspaceSubscription: UpsertWorkspaceSubscription
|
||||
countWorkspaceRole: CountWorkspaceRoleWithOptionalProjectRole
|
||||
upsertWorkspacePlan: UpsertPaidWorkspacePlan
|
||||
}) =>
|
||||
async ({
|
||||
workspaceId,
|
||||
targetPlan,
|
||||
billingInterval
|
||||
}: {
|
||||
workspaceId: string
|
||||
targetPlan: PaidWorkspacePlans
|
||||
billingInterval: WorkspacePlanBillingIntervals
|
||||
}) => {
|
||||
const workspacePlan = await getWorkspacePlan({ workspaceId })
|
||||
|
||||
if (!workspacePlan) throw new WorkspacePlanNotFoundError()
|
||||
switch (workspacePlan.name) {
|
||||
case 'unlimited':
|
||||
case 'academia':
|
||||
case 'starterInvoiced':
|
||||
case 'plusInvoiced':
|
||||
case 'businessInvoiced':
|
||||
case 'teamUnlimitedInvoiced':
|
||||
case 'proUnlimitedInvoiced':
|
||||
case 'free': // TODO: Don't we want to allow upgrades from free to paid?
|
||||
throw new WorkspaceNotPaidPlanError()
|
||||
case 'starter':
|
||||
case 'plus':
|
||||
case 'business':
|
||||
break
|
||||
case 'team':
|
||||
case 'teamUnlimited':
|
||||
case 'pro':
|
||||
case 'proUnlimited':
|
||||
throw new WorkspacePlanMismatchError()
|
||||
default:
|
||||
throwUncoveredError(workspacePlan)
|
||||
}
|
||||
|
||||
switch (workspacePlan.status) {
|
||||
case 'canceled':
|
||||
case 'cancelationScheduled':
|
||||
case 'paymentFailed':
|
||||
case 'trial':
|
||||
case 'expired':
|
||||
throw new WorkspaceNotPaidPlanError()
|
||||
case 'valid':
|
||||
break
|
||||
default:
|
||||
throwUncoveredError(workspacePlan)
|
||||
}
|
||||
|
||||
const workspaceSubscription = await getWorkspaceSubscription({ workspaceId })
|
||||
if (!workspaceSubscription) throw new WorkspaceSubscriptionNotFoundError()
|
||||
|
||||
const planOrder: Record<PaidWorkspacePlans, number> = {
|
||||
// old
|
||||
business: 3,
|
||||
plus: 2,
|
||||
starter: 1,
|
||||
// new
|
||||
team: 1,
|
||||
teamUnlimited: 2,
|
||||
pro: 3,
|
||||
proUnlimited: 4
|
||||
}
|
||||
|
||||
if (isNewPlanType(workspacePlan.name) || isNewPlanType(targetPlan)) {
|
||||
throw new NotImplementedError()
|
||||
}
|
||||
|
||||
const planCheckers = [isNewPlanType, isOldPaidPlanType]
|
||||
for (const isSpecificPlanType of planCheckers) {
|
||||
const oldPlanFitsSchema = isSpecificPlanType(workspacePlan.name)
|
||||
const newPlanFitsSchema = isSpecificPlanType(targetPlan)
|
||||
if (xor(oldPlanFitsSchema, newPlanFitsSchema)) {
|
||||
throw new WorkspacePlanUpgradeError(
|
||||
'Attempting to switch between incompatible plan types'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isNewPlanType(targetPlan) || isNewPlanType(workspacePlan.name)) {
|
||||
// Needs custom logic below for seats
|
||||
throw new NotImplementedError()
|
||||
}
|
||||
|
||||
if (
|
||||
planOrder[workspacePlan.name] === planOrder[targetPlan] &&
|
||||
workspaceSubscription.billingInterval === billingInterval
|
||||
)
|
||||
throw new WorkspacePlanUpgradeError("Can't upgrade to the same plan")
|
||||
|
||||
if (planOrder[workspacePlan.name] > planOrder[targetPlan])
|
||||
throw new WorkspacePlanUpgradeError("Can't upgrade to a less expensive plan")
|
||||
|
||||
switch (billingInterval) {
|
||||
case 'monthly':
|
||||
if (workspaceSubscription.billingInterval === 'yearly')
|
||||
throw new WorkspacePlanUpgradeError(
|
||||
"Can't upgrade from yearly to monthly billing cycle"
|
||||
)
|
||||
case 'yearly':
|
||||
break
|
||||
default:
|
||||
throwUncoveredError(billingInterval)
|
||||
}
|
||||
|
||||
const subscriptionData: SubscriptionDataInput = cloneDeep(
|
||||
workspaceSubscription.subscriptionData
|
||||
)
|
||||
|
||||
const product = subscriptionData.products.find(
|
||||
(p) =>
|
||||
p.productId === getWorkspacePlanProductId({ workspacePlan: workspacePlan.name })
|
||||
)
|
||||
if (!product) throw new WorkspacePlanMismatchError()
|
||||
|
||||
const [guestCount, memberCount, adminCount] = await Promise.all([
|
||||
countWorkspaceRole({ workspaceId, workspaceRole: 'workspace:guest' }),
|
||||
countWorkspaceRole({ workspaceId, workspaceRole: 'workspace:member' }),
|
||||
countWorkspaceRole({ workspaceId, workspaceRole: 'workspace:admin' })
|
||||
])
|
||||
|
||||
workspaceSubscription.updatedAt = new Date()
|
||||
if (workspaceSubscription.billingInterval !== billingInterval) {
|
||||
workspaceSubscription.billingInterval = billingInterval
|
||||
workspaceSubscription.currentBillingCycleEnd = calculateNewBillingCycleEnd({
|
||||
workspaceSubscription
|
||||
})
|
||||
const guestProduct = subscriptionData.products.find(
|
||||
(p) => p.productId === getWorkspacePlanProductId({ workspacePlan: 'guest' })
|
||||
)
|
||||
if (guestProduct) {
|
||||
mutateSubscriptionDataWithNewValidSeatNumbers({
|
||||
seatCount: 0,
|
||||
getWorkspacePlanProductId,
|
||||
subscriptionData,
|
||||
workspacePlan: 'guest'
|
||||
})
|
||||
|
||||
subscriptionData.products.push({
|
||||
quantity: guestCount,
|
||||
productId: getWorkspacePlanProductId({ workspacePlan: 'guest' }),
|
||||
priceId: getWorkspacePlanPriceId({
|
||||
workspacePlan: 'guest',
|
||||
billingInterval,
|
||||
currency: workspaceSubscription.currency
|
||||
}),
|
||||
subscriptionItemId: undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// set current plan seat count to 0
|
||||
mutateSubscriptionDataWithNewValidSeatNumbers({
|
||||
seatCount: 0,
|
||||
getWorkspacePlanProductId,
|
||||
subscriptionData,
|
||||
workspacePlan: workspacePlan.name
|
||||
})
|
||||
|
||||
// set target plan seat count to current seat count
|
||||
subscriptionData.products.push({
|
||||
quantity: memberCount + adminCount,
|
||||
productId: getWorkspacePlanProductId({ workspacePlan: targetPlan }),
|
||||
priceId: getWorkspacePlanPriceId({
|
||||
workspacePlan: targetPlan,
|
||||
billingInterval,
|
||||
currency: workspaceSubscription.currency
|
||||
}),
|
||||
subscriptionItemId: undefined
|
||||
})
|
||||
|
||||
await reconcileSubscriptionData({
|
||||
subscriptionData,
|
||||
prorationBehavior: isNewPlanType(targetPlan)
|
||||
? 'always_invoice'
|
||||
: 'create_prorations'
|
||||
})
|
||||
await upsertWorkspacePlan({
|
||||
workspacePlan: {
|
||||
status: workspacePlan.status,
|
||||
workspaceId,
|
||||
name: targetPlan,
|
||||
createdAt: new Date()
|
||||
}
|
||||
})
|
||||
await updateWorkspaceSubscription({ workspaceSubscription })
|
||||
}
|
||||
|
||||
export const upgradeWorkspaceSubscriptionFactoryNew =
|
||||
export const upgradeWorkspaceSubscriptionFactory =
|
||||
({
|
||||
getWorkspacePlan,
|
||||
getWorkspacePlanProductId,
|
||||
@@ -282,16 +67,9 @@ export const upgradeWorkspaceSubscriptionFactoryNew =
|
||||
case 'unlimited':
|
||||
case 'academia':
|
||||
case 'teamUnlimitedInvoiced':
|
||||
case 'businessInvoiced':
|
||||
case 'plusInvoiced':
|
||||
case 'starterInvoiced':
|
||||
case 'proUnlimitedInvoiced':
|
||||
case 'free': // Upgrade from free is handled through startCheckout since it is from free to paid
|
||||
throw new WorkspaceNotPaidPlanError()
|
||||
case 'starter':
|
||||
case 'plus':
|
||||
case 'business':
|
||||
throw new WorkspacePlanMismatchError()
|
||||
case 'team':
|
||||
case 'teamUnlimited':
|
||||
case 'pro':
|
||||
@@ -301,7 +79,7 @@ export const upgradeWorkspaceSubscriptionFactoryNew =
|
||||
throwUncoveredError(workspacePlan)
|
||||
}
|
||||
|
||||
if (!isNewPlanType(workspacePlan.name) || !isNewPaidPlanType(targetPlan)) {
|
||||
if (!isPaidPlanType(targetPlan)) {
|
||||
throw new UnsupportedWorkspacePlanError(null, {
|
||||
info: { currentPlan: workspacePlan.name, targetPlan }
|
||||
})
|
||||
@@ -327,7 +105,7 @@ export const upgradeWorkspaceSubscriptionFactoryNew =
|
||||
)
|
||||
throw new WorkspacePlanUpgradeError("Can't upgrade to the same plan")
|
||||
|
||||
const planOrder: Record<PaidWorkspacePlansNew, number> = {
|
||||
const planOrder: Record<PaidWorkspacePlans, number> = {
|
||||
team: 1,
|
||||
teamUnlimited: 2,
|
||||
pro: 3,
|
||||
@@ -336,9 +114,7 @@ export const upgradeWorkspaceSubscriptionFactoryNew =
|
||||
if (
|
||||
!isUpgradeWorkspacePlanValid({ current: workspacePlan.name, upgrade: targetPlan })
|
||||
) {
|
||||
if (
|
||||
planOrder[workspacePlan.name] > planOrder[targetPlan as PaidWorkspacePlansNew]
|
||||
) {
|
||||
if (planOrder[workspacePlan.name] > planOrder[targetPlan]) {
|
||||
throw new WorkspacePlanUpgradeError("Can't upgrade to a less expensive plan")
|
||||
}
|
||||
throw new InvalidWorkspacePlanUpgradeError(null, {
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
import { GetServerInfo } from '@/modules/core/domain/server/operations'
|
||||
import { FindEmailsByUserId } from '@/modules/core/domain/userEmails/operations'
|
||||
import {
|
||||
EmailTemplateParams,
|
||||
RenderEmail,
|
||||
SendEmail,
|
||||
SendEmailParams
|
||||
} from '@/modules/emails/domain/operations'
|
||||
import { getServerOrigin } from '@/modules/shared/helpers/envHelper'
|
||||
import { mixpanel } from '@/modules/shared/utils/mixpanel'
|
||||
import { GetWorkspaceCollaborators } from '@/modules/workspaces/domain/operations'
|
||||
import { WorkspaceTeamMember } from '@/modules/workspaces/domain/types'
|
||||
import { Workspace } from '@/modules/workspacesCore/domain/types'
|
||||
import { Roles } from '@speckle/shared'
|
||||
|
||||
type TrialExpiresArgs = {
|
||||
workspace: Workspace
|
||||
expiresInDays: number
|
||||
}
|
||||
|
||||
type TrialExpiresArgsWithAdmin = TrialExpiresArgs & {
|
||||
workspaceAdmin: WorkspaceTeamMember
|
||||
}
|
||||
|
||||
const buildMjmlBody = ({
|
||||
workspace,
|
||||
expiresInDays,
|
||||
workspaceAdmin
|
||||
}: TrialExpiresArgsWithAdmin) => {
|
||||
const expireMessage =
|
||||
expiresInDays === 0
|
||||
? `<strong>today</strong>`
|
||||
: `in <strong>${expiresInDays} days</strong>`
|
||||
const bodyStart = `<mj-text align="center" line-height="2">
|
||||
Hi ${workspaceAdmin.name}!
|
||||
<br/>
|
||||
<br/>
|
||||
The trial for your workspace <span style="font-variant: small-caps; font-weight: bold;">${workspace.name}</span> expires ${expireMessage}.
|
||||
<br/>
|
||||
Upgrade to a paid plan before the trial expires to keep using your workspace. You can compare plans and get an overview of your estimated billing from your workspace's billing settings.
|
||||
</mj-text>
|
||||
`
|
||||
const bodyEnd = `<mj-text align="center" padding-bottom="0px" line-height="2">
|
||||
<span style="font-weight: bold;">Have questions or feedback?</span><br/>Please write us at <a href="mailto:hello@speckle.systems" target="_blank">hello@speckle.systems</a> and we'd be more than happy to talk.
|
||||
</mj-text>`
|
||||
return { bodyStart, bodyEnd }
|
||||
}
|
||||
|
||||
const buildTextBody = ({
|
||||
workspace,
|
||||
expiresInDays,
|
||||
workspaceAdmin
|
||||
}: TrialExpiresArgsWithAdmin) => {
|
||||
const expireMessage = expiresInDays === 0 ? `today` : `in ${expiresInDays} days`
|
||||
const bodyStart = `
|
||||
|
||||
Hi ${workspaceAdmin.name}!
|
||||
\r\n\r\n
|
||||
The trial for your workspace ${workspace.name} expires ${expireMessage}.
|
||||
\r\n\r\n
|
||||
Upgrade to a paid plan before the trial expires to keep using your workspace. You can compare plans and get an overview of your estimated billing from your workspace's billing settings.
|
||||
\r\n\r\n
|
||||
`
|
||||
const bodyEnd = `Have questions or feedback? Please write us at hello@speckle.systems and we'd be more than happy to talk.`
|
||||
return { bodyStart, bodyEnd }
|
||||
}
|
||||
|
||||
const buildEmailTemplateParams = (
|
||||
args: TrialExpiresArgsWithAdmin
|
||||
): EmailTemplateParams => {
|
||||
const url = new URL(`workspaces/${args.workspace.slug}`, getServerOrigin()).toString()
|
||||
return {
|
||||
mjml: buildMjmlBody(args),
|
||||
text: buildTextBody(args),
|
||||
cta: {
|
||||
title: 'Upgrade your workspace',
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const sendWorkspaceTrialExpiresEmailFactory =
|
||||
({
|
||||
renderEmail,
|
||||
sendEmail,
|
||||
getServerInfo,
|
||||
getWorkspaceCollaborators,
|
||||
getUserEmails
|
||||
}: {
|
||||
renderEmail: RenderEmail
|
||||
sendEmail: SendEmail
|
||||
getServerInfo: GetServerInfo
|
||||
getWorkspaceCollaborators: GetWorkspaceCollaborators
|
||||
getUserEmails: FindEmailsByUserId
|
||||
}) =>
|
||||
async (args: TrialExpiresArgs) => {
|
||||
const mp = mixpanel({ userEmail: undefined, req: undefined })
|
||||
const [serverInfo, workspaceAdmins] = await Promise.all([
|
||||
getServerInfo(),
|
||||
getWorkspaceCollaborators({
|
||||
workspaceId: args.workspace.id,
|
||||
limit: 100,
|
||||
filter: { roles: [Roles.Workspace.Admin] }
|
||||
})
|
||||
])
|
||||
const sendEmailParams = await Promise.all(
|
||||
workspaceAdmins.map(async (admin) => {
|
||||
const userEmails = await getUserEmails({ userId: admin.id })
|
||||
const emailTemplateParams = buildEmailTemplateParams({
|
||||
...args,
|
||||
workspaceAdmin: admin
|
||||
})
|
||||
const { html, text } = await renderEmail(emailTemplateParams, serverInfo, null)
|
||||
const subject =
|
||||
args.expiresInDays === 0
|
||||
? 'Your workspace trial expires today'
|
||||
: `Your workspace trial expires in ${args.expiresInDays} days`
|
||||
const sendEmailParams: SendEmailParams = {
|
||||
html,
|
||||
text,
|
||||
subject,
|
||||
to: userEmails.map((e) => e.email)
|
||||
}
|
||||
return sendEmailParams
|
||||
})
|
||||
)
|
||||
await Promise.all(sendEmailParams.map((params) => sendEmail(params)))
|
||||
await mp.track('Workspace Trial Expiration Email Sent', {
|
||||
workspaceId: args.workspace.id,
|
||||
// eslint-disable-next-line camelcase
|
||||
workspace_id: args.workspace.id,
|
||||
expiresInDays: args.expiresInDays
|
||||
})
|
||||
}
|
||||
@@ -3,12 +3,6 @@ import { WorkspacePlans } from '@speckle/shared'
|
||||
const WorkspacePlansUpgradeMapping: Record<WorkspacePlans, WorkspacePlans[]> = {
|
||||
academia: [],
|
||||
unlimited: [],
|
||||
business: [],
|
||||
businessInvoiced: [],
|
||||
plus: [],
|
||||
plusInvoiced: [],
|
||||
starter: [],
|
||||
starterInvoiced: [],
|
||||
free: ['team', 'teamUnlimited', 'pro', 'proUnlimited'],
|
||||
team: ['team', 'teamUnlimited', 'pro', 'proUnlimited'],
|
||||
teamUnlimited: ['teamUnlimited', 'pro', 'proUnlimited'],
|
||||
|
||||
@@ -28,32 +28,11 @@ export const updateWorkspacePlanFactory =
|
||||
if (!workspace) throw new WorkspaceNotFoundError()
|
||||
const createdAt = new Date()
|
||||
switch (name) {
|
||||
case 'starter':
|
||||
switch (status) {
|
||||
case 'trial':
|
||||
case 'expired':
|
||||
case 'valid':
|
||||
case 'cancelationScheduled':
|
||||
case 'canceled':
|
||||
case 'paymentFailed':
|
||||
await upsertWorkspacePlan({
|
||||
workspacePlan: { workspaceId, status, name, createdAt }
|
||||
})
|
||||
break
|
||||
default:
|
||||
throwUncoveredError(status)
|
||||
}
|
||||
break
|
||||
case 'business':
|
||||
case 'plus':
|
||||
case 'team':
|
||||
case 'teamUnlimited':
|
||||
case 'pro':
|
||||
case 'proUnlimited':
|
||||
switch (status) {
|
||||
case 'trial':
|
||||
case 'expired':
|
||||
throw new InvalidWorkspacePlanStatus()
|
||||
case 'valid':
|
||||
case 'cancelationScheduled':
|
||||
case 'canceled':
|
||||
@@ -70,9 +49,6 @@ export const updateWorkspacePlanFactory =
|
||||
case 'free':
|
||||
case 'academia':
|
||||
case 'unlimited':
|
||||
case 'starterInvoiced':
|
||||
case 'plusInvoiced':
|
||||
case 'businessInvoiced':
|
||||
case 'teamUnlimitedInvoiced':
|
||||
case 'proUnlimitedInvoiced':
|
||||
switch (status) {
|
||||
@@ -83,9 +59,7 @@ export const updateWorkspacePlanFactory =
|
||||
break
|
||||
case 'cancelationScheduled':
|
||||
case 'canceled':
|
||||
case 'expired':
|
||||
case 'paymentFailed':
|
||||
case 'trial':
|
||||
throw new InvalidWorkspacePlanStatus()
|
||||
default:
|
||||
throwUncoveredError(status)
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import {
|
||||
Currency,
|
||||
GetWorkspacePlanPriceId,
|
||||
GetWorkspacePlanProductAndPriceIds,
|
||||
GetWorkspacePlanProductId,
|
||||
isMultiCurrencyPrice
|
||||
GetWorkspacePlanProductId
|
||||
} from '@/modules/gatekeeper/domain/billing'
|
||||
import { getStringFromEnv, getStripeApiKey } from '@/modules/shared/helpers/envHelper'
|
||||
import { PriceLookupError } from '@/modules/gatekeeper/errors/billing'
|
||||
import { Stripe } from 'stripe'
|
||||
import { NotImplementedError } from '@/modules/shared/errors'
|
||||
|
||||
@@ -18,43 +15,6 @@ export const getStripeClient = () => {
|
||||
}
|
||||
|
||||
const loadProductAndPriceIds: GetWorkspacePlanProductAndPriceIds = () => ({
|
||||
// old
|
||||
guest: {
|
||||
productId: getStringFromEnv('WORKSPACE_GUEST_SEAT_STRIPE_PRODUCT_ID'),
|
||||
monthly: {
|
||||
gbp: getStringFromEnv('WORKSPACE_MONTHLY_GUEST_SEAT_STRIPE_PRICE_ID')
|
||||
},
|
||||
yearly: {
|
||||
gbp: getStringFromEnv('WORKSPACE_YEARLY_GUEST_SEAT_STRIPE_PRICE_ID')
|
||||
}
|
||||
},
|
||||
starter: {
|
||||
productId: getStringFromEnv('WORKSPACE_STARTER_SEAT_STRIPE_PRODUCT_ID'),
|
||||
monthly: {
|
||||
gbp: getStringFromEnv('WORKSPACE_MONTHLY_STARTER_SEAT_STRIPE_PRICE_ID')
|
||||
},
|
||||
yearly: {
|
||||
gbp: getStringFromEnv('WORKSPACE_YEARLY_STARTER_SEAT_STRIPE_PRICE_ID')
|
||||
}
|
||||
},
|
||||
plus: {
|
||||
productId: getStringFromEnv('WORKSPACE_PLUS_SEAT_STRIPE_PRODUCT_ID'),
|
||||
monthly: {
|
||||
gbp: getStringFromEnv('WORKSPACE_MONTHLY_PLUS_SEAT_STRIPE_PRICE_ID')
|
||||
},
|
||||
yearly: {
|
||||
gbp: getStringFromEnv('WORKSPACE_YEARLY_PLUS_SEAT_STRIPE_PRICE_ID')
|
||||
}
|
||||
},
|
||||
business: {
|
||||
productId: getStringFromEnv('WORKSPACE_BUSINESS_SEAT_STRIPE_PRODUCT_ID'),
|
||||
monthly: {
|
||||
gbp: getStringFromEnv('WORKSPACE_MONTHLY_BUSINESS_SEAT_STRIPE_PRICE_ID')
|
||||
},
|
||||
yearly: {
|
||||
gbp: getStringFromEnv('WORKSPACE_YEARLY_BUSINESS_SEAT_STRIPE_PRICE_ID')
|
||||
}
|
||||
},
|
||||
team: {
|
||||
productId: getStringFromEnv('WORKSPACE_TEAM_SEAT_STRIPE_PRODUCT_ID'),
|
||||
monthly: {
|
||||
@@ -118,13 +78,6 @@ export const getWorkspacePlanPriceId: GetWorkspacePlanPriceId = ({
|
||||
}) => {
|
||||
const plan = getWorkspacePlanProductAndPriceIds()[workspacePlan]
|
||||
const priceIds = plan[billingInterval]
|
||||
if (!isMultiCurrencyPrice(priceIds)) {
|
||||
if (currency !== Currency.gbp)
|
||||
throw new PriceLookupError(
|
||||
`Plan '${workspacePlan}' does not have a ${billingInterval} price for currency ${currency}`
|
||||
)
|
||||
return priceIds[currency]
|
||||
}
|
||||
return priceIds[currency]
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,9 @@ import {
|
||||
upsertPaidWorkspacePlanFactory,
|
||||
getWorkspaceSubscriptionFactory,
|
||||
getWorkspaceSubscriptionBySubscriptionIdFactory,
|
||||
changeExpiredTrialWorkspacePlanStatusesFactory,
|
||||
upsertTrialWorkspacePlanFactory,
|
||||
getWorkspacesByPlanAgeFactory,
|
||||
getWorkspaceSubscriptionsPastBillingCycleEndFactoryOldPlans,
|
||||
upsertWorkspacePlanFactory
|
||||
upsertWorkspacePlanFactory,
|
||||
getWorkspaceSubscriptionsPastBillingCycleEndFactory
|
||||
} from '@/modules/gatekeeper/repositories/billing'
|
||||
import {
|
||||
createTestSubscriptionData,
|
||||
@@ -23,6 +21,7 @@ import {
|
||||
import { upsertWorkspaceFactory } from '@/modules/workspaces/repositories/workspaces'
|
||||
import { truncateTables } from '@/test/hooks'
|
||||
import { createAndStoreTestWorkspaceFactory } from '@/test/speckle-helpers/workspaces'
|
||||
import { PaidWorkspacePlans } from '@speckle/shared'
|
||||
import { expect } from 'chai'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import { beforeEach } from 'mocha'
|
||||
@@ -33,7 +32,6 @@ const createAndStoreTestWorkspace = createAndStoreTestWorkspaceFactory({
|
||||
})
|
||||
const getWorkspacePlan = getWorkspacePlanFactory({ db })
|
||||
const upsertPaidWorkspacePlan = upsertPaidWorkspacePlanFactory({ db })
|
||||
const upsertTrialWorkspacePlan = upsertTrialWorkspacePlanFactory({ db })
|
||||
const saveCheckoutSession = saveCheckoutSessionFactory({ db })
|
||||
const deleteCheckoutSession = deleteCheckoutSessionFactory({ db })
|
||||
const getCheckoutSession = getCheckoutSessionFactory({ db })
|
||||
@@ -43,14 +41,11 @@ const upsertWorkspaceSubscription = upsertWorkspaceSubscriptionFactory({ db })
|
||||
const getWorkspaceSubscription = getWorkspaceSubscriptionFactory({ db })
|
||||
const getWorkspaceSubscriptionBySubscriptionId =
|
||||
getWorkspaceSubscriptionBySubscriptionIdFactory({ db })
|
||||
|
||||
const getSubscriptionsAboutToEndBillingCycleOld =
|
||||
getWorkspaceSubscriptionsPastBillingCycleEndFactoryOldPlans({ db })
|
||||
|
||||
const changeExpiredTrialWorkspacePlanStatuses =
|
||||
changeExpiredTrialWorkspacePlanStatusesFactory({ db })
|
||||
const getWorkspacesByPlanAge = getWorkspacesByPlanAgeFactory({ db })
|
||||
|
||||
const getWorkspaceSubscriptionsPastBillingCycleEnd =
|
||||
getWorkspaceSubscriptionsPastBillingCycleEndFactory({ db })
|
||||
|
||||
describe('billing repositories @gatekeeper', () => {
|
||||
describe('workspacePlans', () => {
|
||||
beforeEach(async () => {
|
||||
@@ -63,7 +58,7 @@ describe('billing repositories @gatekeeper', () => {
|
||||
let storedWorkspacePlan = await getWorkspacePlan({ workspaceId })
|
||||
expect(storedWorkspacePlan).to.be.null
|
||||
const workspacePlan = {
|
||||
name: 'business',
|
||||
name: PaidWorkspacePlans.Team,
|
||||
status: 'paymentFailed',
|
||||
workspaceId,
|
||||
createdAt: new Date()
|
||||
@@ -79,7 +74,7 @@ describe('billing repositories @gatekeeper', () => {
|
||||
const workspace = await createAndStoreTestWorkspace()
|
||||
const workspaceId = workspace.id
|
||||
const workspacePlan = {
|
||||
name: 'business',
|
||||
name: PaidWorkspacePlans.Team,
|
||||
status: 'paymentFailed',
|
||||
createdAt: new Date(),
|
||||
workspaceId
|
||||
@@ -98,97 +93,14 @@ describe('billing repositories @gatekeeper', () => {
|
||||
expect(storedWorkspacePlan).deep.equal(planUpdate)
|
||||
})
|
||||
})
|
||||
describe('changeExpiredTrialWorkspacePlanStatusesFactory creates a function, that', () => {
|
||||
it('ignores non trial plans', async () => {
|
||||
const workspace = await createAndStoreTestWorkspace()
|
||||
await upsertPaidWorkspacePlan({
|
||||
workspacePlan: {
|
||||
name: 'business',
|
||||
status: 'cancelationScheduled',
|
||||
workspaceId: workspace.id,
|
||||
createdAt: new Date(2023, 0, 1)
|
||||
}
|
||||
})
|
||||
|
||||
const expiredPlans = await changeExpiredTrialWorkspacePlanStatuses({
|
||||
numberOfDays: 1
|
||||
})
|
||||
expect(expiredPlans.map((p) => p.workspaceId).includes(workspace.id)).to.be
|
||||
.false
|
||||
})
|
||||
it('ignores non expired trial plans', async () => {
|
||||
const workspace = await createAndStoreTestWorkspace()
|
||||
await upsertTrialWorkspacePlan({
|
||||
workspacePlan: {
|
||||
name: 'starter',
|
||||
status: 'trial',
|
||||
workspaceId: workspace.id,
|
||||
createdAt: new Date()
|
||||
}
|
||||
})
|
||||
|
||||
const expiredPlans = await changeExpiredTrialWorkspacePlanStatuses({
|
||||
numberOfDays: 1
|
||||
})
|
||||
expect(expiredPlans.map((p) => p.workspaceId).includes(workspace.id)).to.be
|
||||
.false
|
||||
})
|
||||
it('changes status to expired for expired trial plans', async () => {
|
||||
const workspace1 = await createAndStoreTestWorkspace()
|
||||
await upsertTrialWorkspacePlan({
|
||||
workspacePlan: {
|
||||
name: 'starter',
|
||||
status: 'trial',
|
||||
workspaceId: workspace1.id,
|
||||
createdAt: new Date(2023, 0, 1)
|
||||
}
|
||||
})
|
||||
|
||||
const workspace2 = await createAndStoreTestWorkspace()
|
||||
await upsertTrialWorkspacePlan({
|
||||
workspacePlan: {
|
||||
name: 'starter',
|
||||
status: 'trial',
|
||||
workspaceId: workspace2.id,
|
||||
createdAt: new Date(2023, 0, 1)
|
||||
}
|
||||
})
|
||||
|
||||
const workspace3 = await createAndStoreTestWorkspace()
|
||||
const workspace3Plan = {
|
||||
name: 'starter',
|
||||
status: 'trial',
|
||||
workspaceId: workspace3.id,
|
||||
createdAt: new Date()
|
||||
} as const
|
||||
await upsertTrialWorkspacePlan({
|
||||
workspacePlan: workspace3Plan
|
||||
})
|
||||
|
||||
const expiredPlans = await changeExpiredTrialWorkspacePlanStatuses({
|
||||
numberOfDays: 1
|
||||
})
|
||||
const expiredWorkspaceIds = expiredPlans.map((p) => p.workspaceId)
|
||||
expect(expiredWorkspaceIds.includes(workspace1.id)).to.be.true
|
||||
expect(expiredWorkspaceIds.includes(workspace2.id)).to.be.true
|
||||
expect(expiredWorkspaceIds.includes(workspace3.id)).to.be.false
|
||||
expiredPlans.forEach((expiredPlan) => {
|
||||
expect(expiredPlan.status).to.equal('expired')
|
||||
})
|
||||
|
||||
const storedWorkspacePlan = await getWorkspacePlan({
|
||||
workspaceId: workspace3.id
|
||||
})
|
||||
expect(storedWorkspacePlan).deep.equal(workspace3Plan)
|
||||
})
|
||||
})
|
||||
describe('getWorkspaceByPlanAgeFactory returns a function, that', () => {
|
||||
it('gets workspace where days to expire matches expected', async () => {
|
||||
const workspace1 = await createAndStoreTestWorkspace()
|
||||
const createdAt1 = new Date()
|
||||
createdAt1.setHours(createdAt1.getHours() - 22)
|
||||
const workspace1Plan = {
|
||||
name: 'business',
|
||||
name: PaidWorkspacePlans.Team,
|
||||
status: 'paymentFailed',
|
||||
createdAt: createdAt1,
|
||||
workspaceId: workspace1.id
|
||||
@@ -200,7 +112,7 @@ describe('billing repositories @gatekeeper', () => {
|
||||
const createdAt2 = new Date()
|
||||
createdAt2.setHours(createdAt2.getHours() - 2)
|
||||
const workspacePlan = {
|
||||
name: 'business',
|
||||
name: PaidWorkspacePlans.Team,
|
||||
status: 'paymentFailed',
|
||||
createdAt: createdAt2,
|
||||
workspaceId: workspace2.id
|
||||
@@ -225,7 +137,7 @@ describe('billing repositories @gatekeeper', () => {
|
||||
const createdAt1 = new Date()
|
||||
createdAt1.setHours(createdAt1.getHours() - 22)
|
||||
const workspace1Plan = {
|
||||
name: 'business',
|
||||
name: PaidWorkspacePlans.Pro,
|
||||
status: 'paymentFailed',
|
||||
createdAt: createdAt1,
|
||||
workspaceId: workspace1.id
|
||||
@@ -237,7 +149,7 @@ describe('billing repositories @gatekeeper', () => {
|
||||
const createdAt2 = new Date()
|
||||
createdAt2.setHours(createdAt2.getHours() - 2)
|
||||
const workspace2Plan = {
|
||||
name: 'starter',
|
||||
name: PaidWorkspacePlans.Team,
|
||||
status: 'paymentFailed',
|
||||
createdAt: createdAt2,
|
||||
workspaceId: workspace2.id
|
||||
@@ -259,7 +171,7 @@ describe('billing repositories @gatekeeper', () => {
|
||||
const createdAt1 = new Date()
|
||||
createdAt1.setHours(createdAt1.getHours() - 22)
|
||||
const workspace1Plan = {
|
||||
name: 'business',
|
||||
name: PaidWorkspacePlans.Team,
|
||||
status: 'paymentFailed',
|
||||
createdAt: createdAt1,
|
||||
workspaceId: workspace1.id
|
||||
@@ -271,7 +183,7 @@ describe('billing repositories @gatekeeper', () => {
|
||||
const createdAt2 = new Date()
|
||||
createdAt2.setHours(createdAt2.getHours() - 2)
|
||||
const workspace2Plan = {
|
||||
name: 'business',
|
||||
name: PaidWorkspacePlans.Team,
|
||||
status: 'valid',
|
||||
createdAt: createdAt2,
|
||||
workspaceId: workspace2.id
|
||||
@@ -293,7 +205,7 @@ describe('billing repositories @gatekeeper', () => {
|
||||
const createdAt1 = new Date()
|
||||
createdAt1.setHours(createdAt1.getHours() - 25)
|
||||
const workspace1Plan = {
|
||||
name: 'starter',
|
||||
name: PaidWorkspacePlans.Team,
|
||||
status: 'valid',
|
||||
createdAt: createdAt1,
|
||||
workspaceId: workspace1.id
|
||||
@@ -305,7 +217,7 @@ describe('billing repositories @gatekeeper', () => {
|
||||
const createdAt2 = new Date()
|
||||
createdAt2.setHours(createdAt2.getHours() - 2)
|
||||
const workspacePlan2 = {
|
||||
name: 'starter',
|
||||
name: PaidWorkspacePlans.Team,
|
||||
status: 'valid',
|
||||
createdAt: createdAt2,
|
||||
workspaceId: workspace2.id
|
||||
@@ -340,7 +252,7 @@ describe('billing repositories @gatekeeper', () => {
|
||||
url: 'https://example.com',
|
||||
workspaceId,
|
||||
currency: 'usd',
|
||||
workspacePlan: 'business'
|
||||
workspacePlan: PaidWorkspacePlans.Team
|
||||
} as const
|
||||
|
||||
await saveCheckoutSession({
|
||||
@@ -364,7 +276,7 @@ describe('billing repositories @gatekeeper', () => {
|
||||
url: 'https://example.com',
|
||||
workspaceId,
|
||||
currency: 'usd',
|
||||
workspacePlan: 'business'
|
||||
workspacePlan: PaidWorkspacePlans.Team
|
||||
} as const
|
||||
|
||||
await saveCheckoutSession({
|
||||
@@ -397,7 +309,7 @@ describe('billing repositories @gatekeeper', () => {
|
||||
url: 'https://example.com',
|
||||
workspaceId,
|
||||
currency: 'usd',
|
||||
workspacePlan: 'business'
|
||||
workspacePlan: PaidWorkspacePlans.Team
|
||||
} as const
|
||||
|
||||
await saveCheckoutSession({
|
||||
@@ -439,7 +351,7 @@ describe('billing repositories @gatekeeper', () => {
|
||||
url: 'https://example.com',
|
||||
workspaceId,
|
||||
currency: 'usd',
|
||||
workspacePlan: 'business'
|
||||
workspacePlan: PaidWorkspacePlans.Team
|
||||
} as const
|
||||
|
||||
await saveCheckoutSession({
|
||||
@@ -534,7 +446,7 @@ describe('billing repositories @gatekeeper', () => {
|
||||
await upsertWorkspacePlanFactory({ db })({
|
||||
workspacePlan: {
|
||||
workspaceId: workspace2Subscription.workspaceId,
|
||||
name: 'plus',
|
||||
name: PaidWorkspacePlans.Team,
|
||||
status: 'valid',
|
||||
createdAt: new Date()
|
||||
}
|
||||
@@ -542,7 +454,7 @@ describe('billing repositories @gatekeeper', () => {
|
||||
await upsertWorkspaceSubscription({
|
||||
workspaceSubscription: workspace2Subscription
|
||||
})
|
||||
const subscriptions = await getSubscriptionsAboutToEndBillingCycleOld()
|
||||
const subscriptions = await getWorkspaceSubscriptionsPastBillingCycleEnd()
|
||||
expect(subscriptions).deep.equalInAnyOrder([workspace2Subscription])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
import { GetWorkspacePlanPricesDocument } from '@/test/graphql/generated/graphql'
|
||||
import { TestApolloServer, testApolloServer } from '@/test/graphqlHelper'
|
||||
import { PaidWorkspacePlansNew } from '@speckle/shared'
|
||||
import { PaidWorkspacePlans } from '@speckle/shared'
|
||||
import { expect } from 'chai'
|
||||
import { Currency } from '@/modules/gatekeeper/domain/billing'
|
||||
|
||||
@@ -21,7 +21,7 @@ const { FF_BILLING_INTEGRATION_ENABLED } = getFeatureFlags()
|
||||
it('returns prices', async () => {
|
||||
const res = await getPrices()
|
||||
|
||||
const expectedPlans = [...Object.values(PaidWorkspacePlansNew)]
|
||||
const expectedPlans = [...Object.values(PaidWorkspacePlans)]
|
||||
|
||||
expect(res).to.not.haveGraphQLErrors()
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
} from '@/test/speckle-helpers/branchHelper'
|
||||
import { createTestCommit, createTestObject } from '@/test/speckle-helpers/commitHelper'
|
||||
import { BasicTestStream, createTestStream } from '@/test/speckle-helpers/streamHelper'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { PaidWorkspacePlans, Roles } from '@speckle/shared'
|
||||
import { expect } from 'chai'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import dayjs from 'dayjs'
|
||||
@@ -79,7 +79,7 @@ describe('Workspaces Billing', () => {
|
||||
ownerId: ''
|
||||
}
|
||||
await createTestWorkspace(workspace, testAdminUser, {
|
||||
addPlan: { name: 'business', status: 'valid' }
|
||||
addPlan: { name: PaidWorkspacePlans.Team, status: 'valid' }
|
||||
})
|
||||
|
||||
const res = await apollo.execute(GetWorkspaceDocument, {
|
||||
@@ -89,7 +89,7 @@ describe('Workspaces Billing', () => {
|
||||
expect(res).to.not.haveGraphQLErrors()
|
||||
expect(res.data?.workspace?.readOnly).to.be.false
|
||||
})
|
||||
it('should return true for workspace plan status expired', async () => {
|
||||
it('should return true for workspace plan status canceled', async () => {
|
||||
const workspace = {
|
||||
id: '',
|
||||
name: 'test ws',
|
||||
@@ -97,7 +97,7 @@ describe('Workspaces Billing', () => {
|
||||
ownerId: ''
|
||||
}
|
||||
await createTestWorkspace(workspace, testAdminUser, {
|
||||
addPlan: { name: 'business', status: 'expired' }
|
||||
addPlan: { name: PaidWorkspacePlans.Team, status: 'canceled' }
|
||||
})
|
||||
|
||||
const res = await apollo.execute(GetWorkspaceDocument, {
|
||||
@@ -107,24 +107,6 @@ describe('Workspaces Billing', () => {
|
||||
expect(res).to.not.haveGraphQLErrors()
|
||||
expect(res.data?.workspace?.readOnly).to.be.true
|
||||
})
|
||||
it('should return false for workspace plan status trial', async () => {
|
||||
const workspace = {
|
||||
id: '',
|
||||
name: 'test ws',
|
||||
slug: cryptoRandomString({ length: 10 }),
|
||||
ownerId: ''
|
||||
}
|
||||
await createTestWorkspace(workspace, testAdminUser, {
|
||||
addPlan: { name: 'business', status: 'trial' }
|
||||
})
|
||||
|
||||
const res = await apollo.execute(GetWorkspaceDocument, {
|
||||
workspaceId: workspace.id
|
||||
})
|
||||
|
||||
expect(res).to.not.haveGraphQLErrors()
|
||||
expect(res.data?.workspace?.readOnly).to.be.false
|
||||
})
|
||||
}
|
||||
)
|
||||
;(FF_BILLING_INTEGRATION_ENABLED ? describe : describe.skip)(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
CheckoutSessionNotFoundError,
|
||||
InvalidWorkspacePlanUpgradeError,
|
||||
WorkspaceAlreadyPaidError,
|
||||
WorkspaceCheckoutSessionInProgressError
|
||||
} from '@/modules/gatekeeper/errors/billing'
|
||||
@@ -62,7 +61,7 @@ describe('checkout @gatekeeper', () => {
|
||||
paymentStatus: 'paid',
|
||||
url: 'https://example.com',
|
||||
workspaceId: cryptoRandomString({ length: 10 }),
|
||||
workspacePlan: 'business',
|
||||
workspacePlan: PaidWorkspacePlans.Team,
|
||||
currency: 'usd',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
@@ -98,7 +97,7 @@ describe('checkout @gatekeeper', () => {
|
||||
paymentStatus: 'unpaid',
|
||||
url: 'https://example.com',
|
||||
workspaceId,
|
||||
workspacePlan: 'business',
|
||||
workspacePlan: PaidWorkspacePlans.Team,
|
||||
currency: 'usd',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
@@ -221,42 +220,6 @@ describe('checkout @gatekeeper', () => {
|
||||
)
|
||||
expect(err.name).to.be.equal(new NotFoundError().name)
|
||||
})
|
||||
it('does not allow checkout from old workspace plans', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const err = await expectToThrow(() =>
|
||||
startCheckoutSessionFactory({
|
||||
getWorkspacePlan: async () => ({
|
||||
name: 'plus',
|
||||
status: 'valid',
|
||||
createdAt: new Date(),
|
||||
workspaceId
|
||||
}),
|
||||
getWorkspaceCheckoutSession: () => {
|
||||
expect.fail()
|
||||
},
|
||||
countSeatsByTypeInWorkspace: () => {
|
||||
expect.fail()
|
||||
},
|
||||
createCheckoutSession: () => {
|
||||
expect.fail()
|
||||
},
|
||||
saveCheckoutSession: () => {
|
||||
expect.fail()
|
||||
},
|
||||
deleteCheckoutSession: () => {
|
||||
expect.fail()
|
||||
}
|
||||
})({
|
||||
workspaceId,
|
||||
billingInterval: 'monthly',
|
||||
workspacePlan: 'pro',
|
||||
workspaceSlug: cryptoRandomString({ length: 10 }),
|
||||
isCreateFlow: false,
|
||||
currency: 'usd'
|
||||
})
|
||||
)
|
||||
expect(err.name).to.be.equal(new InvalidWorkspacePlanUpgradeError().name)
|
||||
})
|
||||
it('does not allow checkout for paid workspace plans, that is in a valid state', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const err = await expectToThrow(() =>
|
||||
@@ -346,7 +309,7 @@ describe('checkout @gatekeeper', () => {
|
||||
paymentStatus: 'unpaid',
|
||||
url: '',
|
||||
workspaceId,
|
||||
workspacePlan: 'business',
|
||||
workspacePlan: PaidWorkspacePlans.Team,
|
||||
currency: 'usd',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { canWorkspaceAccessFeatureFactory } from '@/modules/gatekeeper/services/featureAuthorization'
|
||||
import { WorkspacePlan } from '@speckle/shared'
|
||||
import { PaidWorkspacePlans, WorkspacePlanFeatures } from '@speckle/shared'
|
||||
import { expect } from 'chai'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
|
||||
@@ -18,24 +18,28 @@ describe('featureAuthorization @gatekeeper', () => {
|
||||
})
|
||||
;(
|
||||
[
|
||||
['starter', 'expired', 'oidcSso', false],
|
||||
['starter', 'valid', 'oidcSso', false],
|
||||
['starter', 'valid', 'workspaceDataRegionSpecificity', false],
|
||||
['plus', 'valid', 'workspaceDataRegionSpecificity', false],
|
||||
['plus', 'canceled', 'oidcSso', false],
|
||||
['plus', 'valid', 'oidcSso', true],
|
||||
['business', 'valid', 'workspaceDataRegionSpecificity', true]
|
||||
[PaidWorkspacePlans.Team, 'canceled', WorkspacePlanFeatures.SSO, false],
|
||||
[PaidWorkspacePlans.Team, 'valid', WorkspacePlanFeatures.SSO, false],
|
||||
[
|
||||
PaidWorkspacePlans.Team,
|
||||
'valid',
|
||||
WorkspacePlanFeatures.CustomDataRegion,
|
||||
false
|
||||
],
|
||||
[PaidWorkspacePlans.Pro, 'canceled', WorkspacePlanFeatures.SSO, false],
|
||||
[PaidWorkspacePlans.Pro, 'valid', WorkspacePlanFeatures.SSO, true],
|
||||
[PaidWorkspacePlans.Pro, 'valid', WorkspacePlanFeatures.CustomDataRegion, true]
|
||||
] as const
|
||||
).forEach(([plan, status, workspaceFeature, expectedResult]) => {
|
||||
it(`returns ${expectedResult} for ${plan} @ ${status} for ${workspaceFeature}`, async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const canWorkspaceAccessFeature = canWorkspaceAccessFeatureFactory({
|
||||
getWorkspacePlan: async () =>
|
||||
({
|
||||
name: plan,
|
||||
status,
|
||||
workspaceId
|
||||
} as WorkspacePlan)
|
||||
getWorkspacePlan: async () => ({
|
||||
name: plan,
|
||||
status,
|
||||
workspaceId,
|
||||
createdAt: new Date()
|
||||
})
|
||||
})
|
||||
const result = await canWorkspaceAccessFeature({
|
||||
workspaceId,
|
||||
|
||||
@@ -8,14 +8,6 @@ import { expect } from 'chai'
|
||||
|
||||
describe('@gatekeeper readOnly', () => {
|
||||
describe('isWorkspaceReadOnlyFactory returns a function that', () => {
|
||||
it('returns true if workspace plan status is expired', async () => {
|
||||
const getWorkspacePlan: GetWorkspacePlan = () =>
|
||||
({ status: 'expired' } as unknown as ReturnType<GetWorkspacePlan>)
|
||||
|
||||
const isWorkspaceReadOnly = isWorkspaceReadOnlyFactory({ getWorkspacePlan })
|
||||
|
||||
expect(await isWorkspaceReadOnly({ workspaceId: '' })).to.be.true
|
||||
})
|
||||
it('returns true if workspace plan status is paymentFailed', async () => {
|
||||
const getWorkspacePlan: GetWorkspacePlan = () =>
|
||||
({ status: 'paymentFailed' } as unknown as ReturnType<GetWorkspacePlan>)
|
||||
@@ -32,14 +24,6 @@ describe('@gatekeeper readOnly', () => {
|
||||
|
||||
expect(await isWorkspaceReadOnly({ workspaceId: '' })).to.be.true
|
||||
})
|
||||
it('returns false if workspace plan status is trial', async () => {
|
||||
const getWorkspacePlan: GetWorkspacePlan = () =>
|
||||
({ status: 'trial' } as unknown as ReturnType<GetWorkspacePlan>)
|
||||
|
||||
const isWorkspaceReadOnly = isWorkspaceReadOnlyFactory({ getWorkspacePlan })
|
||||
|
||||
expect(await isWorkspaceReadOnly({ workspaceId: '' })).to.be.false
|
||||
})
|
||||
it('returns false if workspace plan status is valid', async () => {
|
||||
const getWorkspacePlan: GetWorkspacePlan = () =>
|
||||
({ status: 'valid' } as unknown as ReturnType<GetWorkspacePlan>)
|
||||
@@ -68,18 +52,6 @@ describe('@gatekeeper readOnly', () => {
|
||||
|
||||
expect(await isProjectReadOnly({ projectId: '' })).to.be.false
|
||||
})
|
||||
it('returns true if workspace plan status is expired', async () => {
|
||||
const getWorkspacePlanByProjectId: GetWorkspacePlanByProjectId = async () =>
|
||||
({
|
||||
status: 'expired'
|
||||
} as unknown as ReturnType<GetWorkspacePlanByProjectId>)
|
||||
|
||||
const isProjectReadOnly = isProjectReadOnlyFactory({
|
||||
getWorkspacePlanByProjectId
|
||||
})
|
||||
|
||||
expect(await isProjectReadOnly({ projectId: '' })).to.be.true
|
||||
})
|
||||
it('returns true if workspace plan status is paymentFailed', async () => {
|
||||
const getWorkspacePlanByProjectId: GetWorkspacePlanByProjectId = async () =>
|
||||
({
|
||||
@@ -104,18 +76,6 @@ describe('@gatekeeper readOnly', () => {
|
||||
|
||||
expect(await isProjectReadOnly({ projectId: '' })).to.be.true
|
||||
})
|
||||
it('returns false if workspace plan status is trial', async () => {
|
||||
const getWorkspacePlanByProjectId: GetWorkspacePlanByProjectId = async () =>
|
||||
({
|
||||
status: 'trial'
|
||||
} as unknown as ReturnType<GetWorkspacePlanByProjectId>)
|
||||
|
||||
const isProjectReadOnly = isProjectReadOnlyFactory({
|
||||
getWorkspacePlanByProjectId
|
||||
})
|
||||
|
||||
expect(await isProjectReadOnly({ projectId: '' })).to.be.false
|
||||
})
|
||||
it('returns false if workspace plan status is valid', async () => {
|
||||
const getWorkspacePlanByProjectId: GetWorkspacePlanByProjectId = async () =>
|
||||
({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,12 @@ import { EventBusEmit } from '@/modules/shared/services/eventBus'
|
||||
import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace'
|
||||
import { WorkspaceWithOptionalRole } from '@/modules/workspacesCore/domain/types'
|
||||
import { expectToThrow } from '@/test/assertionHelper'
|
||||
import { WorkspacePlan } from '@speckle/shared'
|
||||
import {
|
||||
PaidWorkspacePlans,
|
||||
PaidWorkspacePlanStatuses,
|
||||
UnpaidWorkspacePlans,
|
||||
WorkspacePlan
|
||||
} from '@speckle/shared'
|
||||
import { expect } from 'chai'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import { omit } from 'lodash'
|
||||
@@ -24,8 +29,8 @@ describe('workspacePlan services @gatekeeper', () => {
|
||||
const err = await expectToThrow(async () => {
|
||||
await updateWorkspacePlan({
|
||||
workspaceId: cryptoRandomString({ length: 10 }),
|
||||
name: 'business',
|
||||
status: 'expired'
|
||||
name: PaidWorkspacePlans.Team,
|
||||
status: PaidWorkspacePlanStatuses.Canceled
|
||||
})
|
||||
})
|
||||
expect(err.message).to.equal(new WorkspaceNotFoundError().message)
|
||||
@@ -34,100 +39,97 @@ describe('workspacePlan services @gatekeeper', () => {
|
||||
const invalidPlanMessage = new InvalidWorkspacePlanStatus().message
|
||||
;(
|
||||
[
|
||||
{ planName: 'foobar', cases: [['trial', uncoveredErrorMessage]] },
|
||||
{
|
||||
planName: 'starter',
|
||||
planName: 'foobar',
|
||||
cases: [[PaidWorkspacePlanStatuses.Canceled, uncoveredErrorMessage]]
|
||||
},
|
||||
{
|
||||
planName: PaidWorkspacePlans.Team,
|
||||
cases: [
|
||||
['trial', null],
|
||||
['expired', null],
|
||||
['valid', null],
|
||||
['cancelationScheduled', null],
|
||||
['canceled', null],
|
||||
['paymentFailed', null],
|
||||
[PaidWorkspacePlanStatuses.Valid, null],
|
||||
[PaidWorkspacePlanStatuses.CancelationScheduled, null],
|
||||
[PaidWorkspacePlanStatuses.Canceled, null],
|
||||
[PaidWorkspacePlanStatuses.PaymentFailed, null],
|
||||
['foobar', uncoveredErrorMessage]
|
||||
]
|
||||
},
|
||||
{
|
||||
planName: 'business',
|
||||
planName: PaidWorkspacePlans.Pro,
|
||||
cases: [
|
||||
['trial', invalidPlanMessage],
|
||||
['expired', invalidPlanMessage],
|
||||
['valid', null],
|
||||
['cancelationScheduled', null],
|
||||
['canceled', null],
|
||||
['paymentFailed', null],
|
||||
[PaidWorkspacePlanStatuses.Valid, null],
|
||||
[PaidWorkspacePlanStatuses.CancelationScheduled, null],
|
||||
[PaidWorkspacePlanStatuses.Canceled, null],
|
||||
[PaidWorkspacePlanStatuses.PaymentFailed, null],
|
||||
['foobar', uncoveredErrorMessage]
|
||||
]
|
||||
},
|
||||
{
|
||||
planName: 'plus',
|
||||
planName: PaidWorkspacePlans.TeamUnlimited,
|
||||
cases: [
|
||||
['trial', invalidPlanMessage],
|
||||
['expired', invalidPlanMessage],
|
||||
['valid', null],
|
||||
['cancelationScheduled', null],
|
||||
['canceled', null],
|
||||
['paymentFailed', null],
|
||||
[PaidWorkspacePlanStatuses.Valid, null],
|
||||
[PaidWorkspacePlanStatuses.CancelationScheduled, null],
|
||||
[PaidWorkspacePlanStatuses.Canceled, null],
|
||||
[PaidWorkspacePlanStatuses.PaymentFailed, null],
|
||||
['foobar', uncoveredErrorMessage]
|
||||
]
|
||||
},
|
||||
{
|
||||
planName: 'academia',
|
||||
planName: PaidWorkspacePlans.ProUnlimited,
|
||||
cases: [
|
||||
['valid', null],
|
||||
['trial', invalidPlanMessage],
|
||||
['expired', invalidPlanMessage],
|
||||
['cancelationScheduled', invalidPlanMessage],
|
||||
['canceled', invalidPlanMessage],
|
||||
['paymentFailed', invalidPlanMessage],
|
||||
[PaidWorkspacePlanStatuses.Valid, null],
|
||||
[PaidWorkspacePlanStatuses.CancelationScheduled, null],
|
||||
[PaidWorkspacePlanStatuses.Canceled, null],
|
||||
[PaidWorkspacePlanStatuses.PaymentFailed, null],
|
||||
['foobar', uncoveredErrorMessage]
|
||||
]
|
||||
},
|
||||
{
|
||||
planName: 'unlimited',
|
||||
planName: UnpaidWorkspacePlans.Academia,
|
||||
cases: [
|
||||
['valid', null],
|
||||
['trial', invalidPlanMessage],
|
||||
['expired', invalidPlanMessage],
|
||||
['cancelationScheduled', invalidPlanMessage],
|
||||
['canceled', invalidPlanMessage],
|
||||
['paymentFailed', invalidPlanMessage],
|
||||
[PaidWorkspacePlanStatuses.Valid, null],
|
||||
[PaidWorkspacePlanStatuses.CancelationScheduled, invalidPlanMessage],
|
||||
[PaidWorkspacePlanStatuses.Canceled, invalidPlanMessage],
|
||||
[PaidWorkspacePlanStatuses.PaymentFailed, invalidPlanMessage],
|
||||
['foobar', uncoveredErrorMessage]
|
||||
]
|
||||
},
|
||||
{
|
||||
planName: 'starterInvoiced',
|
||||
planName: UnpaidWorkspacePlans.Free,
|
||||
cases: [
|
||||
['valid', null],
|
||||
['trial', invalidPlanMessage],
|
||||
['expired', invalidPlanMessage],
|
||||
['cancelationScheduled', invalidPlanMessage],
|
||||
['canceled', invalidPlanMessage],
|
||||
['paymentFailed', invalidPlanMessage],
|
||||
[PaidWorkspacePlanStatuses.Valid, null],
|
||||
[PaidWorkspacePlanStatuses.CancelationScheduled, invalidPlanMessage],
|
||||
[PaidWorkspacePlanStatuses.Canceled, invalidPlanMessage],
|
||||
[PaidWorkspacePlanStatuses.PaymentFailed, invalidPlanMessage],
|
||||
['foobar', uncoveredErrorMessage]
|
||||
]
|
||||
},
|
||||
{
|
||||
planName: 'plusInvoiced',
|
||||
planName: UnpaidWorkspacePlans.Unlimited,
|
||||
cases: [
|
||||
['valid', null],
|
||||
['trial', invalidPlanMessage],
|
||||
['expired', invalidPlanMessage],
|
||||
['cancelationScheduled', invalidPlanMessage],
|
||||
['canceled', invalidPlanMessage],
|
||||
['paymentFailed', invalidPlanMessage],
|
||||
[PaidWorkspacePlanStatuses.Valid, null],
|
||||
[PaidWorkspacePlanStatuses.CancelationScheduled, invalidPlanMessage],
|
||||
[PaidWorkspacePlanStatuses.Canceled, invalidPlanMessage],
|
||||
[PaidWorkspacePlanStatuses.PaymentFailed, invalidPlanMessage],
|
||||
['foobar', uncoveredErrorMessage]
|
||||
]
|
||||
},
|
||||
{
|
||||
planName: 'businessInvoiced',
|
||||
planName: UnpaidWorkspacePlans.TeamUnlimitedInvoiced,
|
||||
cases: [
|
||||
['valid', null],
|
||||
['trial', invalidPlanMessage],
|
||||
['expired', invalidPlanMessage],
|
||||
['cancelationScheduled', invalidPlanMessage],
|
||||
['canceled', invalidPlanMessage],
|
||||
['paymentFailed', invalidPlanMessage],
|
||||
[PaidWorkspacePlanStatuses.Valid, null],
|
||||
[PaidWorkspacePlanStatuses.CancelationScheduled, invalidPlanMessage],
|
||||
[PaidWorkspacePlanStatuses.Canceled, invalidPlanMessage],
|
||||
[PaidWorkspacePlanStatuses.PaymentFailed, invalidPlanMessage],
|
||||
['foobar', uncoveredErrorMessage]
|
||||
]
|
||||
},
|
||||
{
|
||||
planName: UnpaidWorkspacePlans.ProUnlimitedInvoiced,
|
||||
cases: [
|
||||
[PaidWorkspacePlanStatuses.Valid, null],
|
||||
[PaidWorkspacePlanStatuses.CancelationScheduled, invalidPlanMessage],
|
||||
[PaidWorkspacePlanStatuses.Canceled, invalidPlanMessage],
|
||||
[PaidWorkspacePlanStatuses.PaymentFailed, invalidPlanMessage],
|
||||
['foobar', uncoveredErrorMessage]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,23 +1,6 @@
|
||||
import {
|
||||
PaidWorkspacePlansOld,
|
||||
PaidWorkspacePlansNew,
|
||||
WorkspacePlanBillingIntervals
|
||||
} from '@speckle/shared'
|
||||
import { PaidWorkspacePlans, WorkspacePlanBillingIntervals } from '@speckle/shared'
|
||||
|
||||
/**
|
||||
* This includes the pricing plans (Stripe products) a customer can sub to
|
||||
// */
|
||||
export type WorkspacePricingProducts =
|
||||
| PaidWorkspacePlansNew
|
||||
| PaidWorkspacePlansOld
|
||||
| 'guest'
|
||||
|
||||
// type WorkspacePlanProductsMetadata<PriceData = string> = Record<
|
||||
// WorkspacePricingProducts,
|
||||
// Record<WorkspacePlanBillingIntervals, PriceData> & {
|
||||
// productId: string
|
||||
// }
|
||||
// >
|
||||
export type WorkspacePricingProducts = PaidWorkspacePlans
|
||||
|
||||
export const Currency = {
|
||||
usd: 'usd',
|
||||
@@ -31,13 +14,7 @@ type IntervalPrices = Record<
|
||||
|
||||
export type WorkspacePlanProductPrices = Record<
|
||||
Currency,
|
||||
Record<PaidWorkspacePlansNew, IntervalPrices>
|
||||
Record<PaidWorkspacePlans, IntervalPrices>
|
||||
>
|
||||
|
||||
export type Currency = (typeof Currency)[keyof typeof Currency]
|
||||
|
||||
// export type WorkspacePlanProductAndPriceIds = WorkspacePlanProductsMetadata<string>
|
||||
// export type WorkspacePlanProductPrices = WorkspacePlanProductsMetadata<{
|
||||
// amount: number
|
||||
// currency: string
|
||||
// }>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Knex } from 'knex'
|
||||
|
||||
/**
|
||||
* The full stripe+db migration should've already executed, this is a fallback for dev/test envs to migrate broken plans
|
||||
*/
|
||||
|
||||
const TABLE_NAME = 'workspace_plans'
|
||||
|
||||
const planMapping = {
|
||||
starter: 'team',
|
||||
starterInvoiced: 'teamUnlimitedInvoiced',
|
||||
plus: 'pro',
|
||||
plusInvoiced: 'proUnlimitedInvoiced',
|
||||
business: 'pro',
|
||||
businessInvoiced: 'proUnlimitedInvoiced'
|
||||
}
|
||||
|
||||
const statusMapping = {
|
||||
trial: 'canceled',
|
||||
expired: 'canceled'
|
||||
}
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
// Migrate plans names
|
||||
for (const [oldPlan, newPlan] of Object.entries(planMapping)) {
|
||||
await knex(TABLE_NAME).where('name', oldPlan).update({ name: newPlan })
|
||||
}
|
||||
|
||||
// Migrate plans statuses
|
||||
for (const [oldStatus, newStatus] of Object.entries(statusMapping)) {
|
||||
await knex(TABLE_NAME).where('status', oldStatus).update({ status: newStatus })
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {
|
||||
// sorry, no going back
|
||||
}
|
||||
@@ -203,10 +203,6 @@ export type UpsertWorkspaceRole = (args: WorkspaceAcl) => Promise<void>
|
||||
/** Service-level change with protection against invalid role changes */
|
||||
export type UpdateWorkspaceRole = (
|
||||
args: Pick<WorkspaceAcl, 'userId' | 'workspaceId' | 'role'> & {
|
||||
/**
|
||||
* If this gets triggered from a project role update, we don't want to override that project's role to the default one
|
||||
*/
|
||||
skipProjectRoleUpdatesFor?: string[]
|
||||
/**
|
||||
* Only add or upgrade role, prevent downgrades
|
||||
*/
|
||||
@@ -242,6 +238,14 @@ export type ValidateWorkspaceMemberProjectRole = (params: {
|
||||
workspaceId: string
|
||||
userId: string
|
||||
projectRole: StreamRoles
|
||||
/**
|
||||
* Instead of resolving actual workspace role/seatType, use this one. Useful when checking
|
||||
* if a planned workspace member will have valid access to a project
|
||||
*/
|
||||
workspaceAccess?: {
|
||||
role: WorkspaceRoles
|
||||
seatType: WorkspaceSeatType
|
||||
}
|
||||
}) => Promise<void>
|
||||
|
||||
/** Workspace Projects */
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
GetWorkspace,
|
||||
GetWorkspaceCollaborators,
|
||||
GetWorkspaceRoleForUser,
|
||||
GetWorkspaceRoleToDefaultProjectRoleMapping,
|
||||
GetWorkspaceSeatTypeToProjectRoleMapping,
|
||||
QueryAllWorkspaceProjects,
|
||||
ValidateWorkspaceMemberProjectRole
|
||||
@@ -76,19 +75,14 @@ import {
|
||||
import { WorkspacesNotAuthorizedError } from '@/modules/workspaces/errors/workspace'
|
||||
import { publish, WorkspaceSubscriptions } from '@/modules/shared/utils/subscriptions'
|
||||
import { isWorkspaceResourceTarget } from '@/modules/workspaces/services/invites'
|
||||
import {
|
||||
ProjectEvents,
|
||||
ProjectEventsPayloads
|
||||
} from '@/modules/core/domain/projects/events'
|
||||
import { ProjectEvents } from '@/modules/core/domain/projects/events'
|
||||
import { getBaseTrackingProperties, getClient } from '@/modules/shared/utils/mixpanel'
|
||||
import {
|
||||
calculateSubscriptionSeats,
|
||||
GetWorkspacePlan,
|
||||
GetWorkspaceRolesAndSeats,
|
||||
GetWorkspaceSubscription,
|
||||
GetWorkspaceWithPlan
|
||||
} from '@/modules/gatekeeper/domain/billing'
|
||||
import { getWorkspacePlanProductId } from '@/modules/gatekeeper/stripe'
|
||||
import { Workspace, WorkspaceSeatType } from '@/modules/workspacesCore/domain/types'
|
||||
import { FindEmailsByUserId } from '@/modules/core/domain/userEmails/operations'
|
||||
import { getDefaultRegionFactory } from '@/modules/workspaces/repositories/regions'
|
||||
@@ -103,7 +97,6 @@ import {
|
||||
createWorkspaceSeatFactory,
|
||||
deleteWorkspaceSeatFactory,
|
||||
getWorkspaceRoleAndSeatFactory,
|
||||
getWorkspaceRolesAndSeatsFactory,
|
||||
getWorkspaceUserSeatFactory
|
||||
} from '@/modules/gatekeeper/repositories/workspaceSeat'
|
||||
import {
|
||||
@@ -117,56 +110,10 @@ import {
|
||||
} from '@/modules/core/services/streams/access'
|
||||
import { getUserFactory } from '@/modules/core/repositories/users'
|
||||
import { authorizeResolver } from '@/modules/shared'
|
||||
import { isNewPlanType } from '@/modules/gatekeeper/helpers/plans'
|
||||
import { getFeatureFlags } from '@/modules/shared/helpers/envHelper'
|
||||
|
||||
const { FF_BILLING_INTEGRATION_ENABLED } = getFeatureFlags()
|
||||
|
||||
export const onProjectCreatedFactory =
|
||||
(deps: {
|
||||
getWorkspaceRolesAndSeats: GetWorkspaceRolesAndSeats
|
||||
upsertProjectRole: UpsertProjectRole
|
||||
getWorkspaceRoleToDefaultProjectRoleMapping: GetWorkspaceRoleToDefaultProjectRoleMapping
|
||||
getWorkspaceWithPlan: GetWorkspaceWithPlan
|
||||
}) =>
|
||||
async (payload: ProjectEventsPayloads[typeof ProjectEvents.Created]) => {
|
||||
const { id: projectId, workspaceId } = payload.project
|
||||
|
||||
if (!workspaceId) {
|
||||
return
|
||||
}
|
||||
|
||||
// Automatic role assignment doesn't apply to new plans
|
||||
const workspace = await deps.getWorkspaceWithPlan({ workspaceId })
|
||||
if (workspace?.plan && isNewPlanType(workspace.plan.name)) return
|
||||
|
||||
const workspaceMembers = Object.values(
|
||||
await deps.getWorkspaceRolesAndSeats({ workspaceId })
|
||||
)
|
||||
|
||||
const { default: defaultProjectRoles } =
|
||||
await deps.getWorkspaceRoleToDefaultProjectRoleMapping({
|
||||
workspaceId
|
||||
})
|
||||
|
||||
// On create assign project roles to all members
|
||||
await Promise.all(
|
||||
workspaceMembers.map(({ userId, role: { role: workspaceRole } }) => {
|
||||
const projectRole = defaultProjectRoles[workspaceRole]
|
||||
if (!projectRole) return
|
||||
|
||||
// we do not need to assign new roles to the project owner
|
||||
if (userId === payload.ownerId) return
|
||||
|
||||
return deps.upsertProjectRole({
|
||||
projectId,
|
||||
userId,
|
||||
role: projectRole
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export const onInviteFinalizedFactory =
|
||||
(deps: {
|
||||
getStream: GetStream
|
||||
@@ -209,8 +156,7 @@ export const onInviteFinalizedFactory =
|
||||
userId: targetUserId,
|
||||
workspaceId: project.workspaceId,
|
||||
preventRoleDowngrade: true,
|
||||
updatedByUserId: invite.inviterId,
|
||||
skipProjectRoleUpdatesFor: [project.id]
|
||||
updatedByUserId: invite.inviterId
|
||||
})
|
||||
|
||||
// Automatically promote user to project owner if workspace admin
|
||||
@@ -351,12 +297,6 @@ export const onWorkspaceSeatUpdatedFactory =
|
||||
])
|
||||
if (!workspace || !role) return
|
||||
|
||||
// Only new plans only rely on seat types
|
||||
const isNewPlan = workspace.plan && isNewPlanType(workspace.plan.name)
|
||||
if (!isNewPlan) {
|
||||
return
|
||||
}
|
||||
|
||||
const { allowed: allowedProjectRoles, default: defaultProjectRoles } =
|
||||
await deps.getWorkspaceSeatTypeToProjectRoleMapping({
|
||||
workspaceId
|
||||
@@ -425,7 +365,6 @@ export const onWorkspaceSeatUpdatedFactory =
|
||||
|
||||
export const onWorkspaceRoleUpdatedFactory =
|
||||
(deps: {
|
||||
getWorkspaceRoleToDefaultProjectRoleMapping: GetWorkspaceRoleToDefaultProjectRoleMapping
|
||||
queryAllWorkspaceProjects: QueryAllWorkspaceProjects
|
||||
setStreamCollaborator: SetStreamCollaborator
|
||||
getWorkspaceUserSeat: GetWorkspaceUserSeat
|
||||
@@ -435,13 +374,9 @@ export const onWorkspaceRoleUpdatedFactory =
|
||||
}) =>
|
||||
async ({
|
||||
acl,
|
||||
updatedByUserId,
|
||||
flags
|
||||
updatedByUserId
|
||||
}: {
|
||||
acl: { userId: string; role: WorkspaceRoles; workspaceId: string }
|
||||
flags?: {
|
||||
skipProjectRoleUpdatesFor: string[]
|
||||
}
|
||||
updatedByUserId: string
|
||||
}) => {
|
||||
const { userId, role, workspaceId } = acl
|
||||
@@ -449,13 +384,6 @@ export const onWorkspaceRoleUpdatedFactory =
|
||||
const workspace = await deps.getWorkspaceWithPlan({ workspaceId })
|
||||
if (!workspace) return
|
||||
|
||||
// Until we kill old plan code, we need to do full project role assignment for them
|
||||
const isOldPlan = !workspace.plan || !isNewPlanType(workspace.plan.name)
|
||||
const { default: defaultProjectRoles } =
|
||||
await deps.getWorkspaceRoleToDefaultProjectRoleMapping({
|
||||
workspaceId
|
||||
})
|
||||
|
||||
const seatType = await deps.getWorkspaceUserSeat({ workspaceId, userId })
|
||||
if (!seatType) return
|
||||
|
||||
@@ -480,13 +408,7 @@ export const onWorkspaceRoleUpdatedFactory =
|
||||
})
|
||||
await Promise.all(
|
||||
projectsPage.map(async ({ id: projectId, role: originalProjectRole }) => {
|
||||
if (isOldPlan && flags?.skipProjectRoleUpdatesFor.includes(projectId)) {
|
||||
// Skip assignment (used during invite flow)
|
||||
// TODO: Can we refactor this special case away?
|
||||
return
|
||||
}
|
||||
|
||||
if (!originalProjectRole && !isOldPlan) {
|
||||
if (!originalProjectRole) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -495,34 +417,30 @@ export const onWorkspaceRoleUpdatedFactory =
|
||||
* been written to DB. So we must ensure the updates we make here are valid
|
||||
*/
|
||||
|
||||
let nextUserRole: StreamRoles | null
|
||||
if (isOldPlan) {
|
||||
nextUserRole = defaultProjectRoles[role]
|
||||
} else {
|
||||
switch (role) {
|
||||
case Roles.Workspace.Admin: {
|
||||
// Set workspace owner as project owner
|
||||
nextUserRole = Roles.Stream.Owner
|
||||
break
|
||||
}
|
||||
case Roles.Workspace.Guest: {
|
||||
// If workspace guest is project owner
|
||||
if (originalProjectRole !== Roles.Stream.Owner) {
|
||||
return
|
||||
}
|
||||
|
||||
// If workspace guest has an editor seat
|
||||
if (seatType.type !== WorkspaceSeatType.Editor) {
|
||||
return
|
||||
}
|
||||
|
||||
// Demote to contributor
|
||||
nextUserRole = Roles.Stream.Contributor
|
||||
break
|
||||
}
|
||||
default:
|
||||
return
|
||||
let nextUserRole: StreamRoles
|
||||
switch (role) {
|
||||
case Roles.Workspace.Admin: {
|
||||
// Set workspace owner as project owner
|
||||
nextUserRole = Roles.Stream.Owner
|
||||
break
|
||||
}
|
||||
case Roles.Workspace.Guest: {
|
||||
// If workspace guest is project owner
|
||||
if (originalProjectRole !== Roles.Stream.Owner) {
|
||||
return
|
||||
}
|
||||
|
||||
// If workspace guest has an editor seat
|
||||
if (seatType.type !== WorkspaceSeatType.Editor) {
|
||||
return
|
||||
}
|
||||
|
||||
// Demote to contributor
|
||||
nextUserRole = Roles.Stream.Contributor
|
||||
break
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
// If downgraded from owner & last owner, transfer ownership to a workspace admin
|
||||
@@ -594,10 +512,9 @@ export const workspaceTrackingFactory =
|
||||
])
|
||||
const seats = subscription?.subscriptionData
|
||||
? calculateSubscriptionSeats({
|
||||
subscriptionData: subscription?.subscriptionData,
|
||||
guestSeatProductId: getWorkspacePlanProductId({ workspacePlan: 'guest' })
|
||||
subscriptionData: subscription?.subscriptionData
|
||||
})
|
||||
: { plan: 0, guest: 0 }
|
||||
: 0
|
||||
return {
|
||||
name: workspace.name,
|
||||
description: workspace.description,
|
||||
@@ -614,8 +531,8 @@ export const workspaceTrackingFactory =
|
||||
planCreatedAt: plan?.createdAt,
|
||||
subscriptionBillingInterval: subscription?.billingInterval,
|
||||
subscriptionCurrentBillingCycleEnd: subscription?.currentBillingCycleEnd,
|
||||
seats: seats.plan,
|
||||
seatsGuest: seats.guest,
|
||||
seats,
|
||||
seatsGuest: 0,
|
||||
...getBaseTrackingProperties()
|
||||
}
|
||||
}
|
||||
@@ -776,13 +693,9 @@ export const initializeEventListenersFactory =
|
||||
validateWorkspaceMemberProjectRole: validateWorkspaceMemberProjectRoleFactory({
|
||||
getWorkspaceRoleAndSeat: getWorkspaceRoleAndSeatFactory({ db }),
|
||||
getWorkspaceRoleToDefaultProjectRoleMapping:
|
||||
getWorkspaceRoleToDefaultProjectRoleMappingFactory({
|
||||
getWorkspaceWithPlan
|
||||
}),
|
||||
getWorkspaceRoleToDefaultProjectRoleMappingFactory(),
|
||||
getWorkspaceSeatTypeToProjectRoleMapping:
|
||||
getWorkspaceSeatTypeToProjectRoleMappingFactory({
|
||||
getWorkspaceWithPlan
|
||||
}),
|
||||
getWorkspaceSeatTypeToProjectRoleMappingFactory(),
|
||||
getWorkspaceWithPlan
|
||||
})
|
||||
})
|
||||
@@ -794,18 +707,6 @@ export const initializeEventListenersFactory =
|
||||
})
|
||||
|
||||
const quitCbs = [
|
||||
eventBus.listen(ProjectEvents.Created, async ({ payload }) => {
|
||||
const onProjectCreated = onProjectCreatedFactory({
|
||||
upsertProjectRole: upsertProjectRoleFactory({ db }),
|
||||
getWorkspaceRolesAndSeats: getWorkspaceRolesAndSeatsFactory({ db }),
|
||||
getWorkspaceRoleToDefaultProjectRoleMapping:
|
||||
getWorkspaceRoleToDefaultProjectRoleMappingFactory({
|
||||
getWorkspaceWithPlan
|
||||
}),
|
||||
getWorkspaceWithPlan
|
||||
})
|
||||
await onProjectCreated(payload)
|
||||
}),
|
||||
eventBus.listen(ServerInvitesEvents.Finalized, async ({ payload }) => {
|
||||
const onInviteFinalized = onInviteFinalizedFactory({
|
||||
getStream: getStreamFactory({ db }),
|
||||
@@ -921,11 +822,7 @@ export const initializeEventListenersFactory =
|
||||
getWorkspaceUserSeat: getWorkspaceUserSeatFactory({ db }),
|
||||
getStreamsCollaboratorCounts: getStreamsCollaboratorCountsFactory({ db }),
|
||||
getWorkspaceCollaborators: getWorkspaceCollaboratorsFactory({ db }),
|
||||
getWorkspaceWithPlan: getWorkspaceWithPlanFactory({ db }),
|
||||
getWorkspaceRoleToDefaultProjectRoleMapping:
|
||||
getWorkspaceRoleToDefaultProjectRoleMappingFactory({
|
||||
getWorkspaceWithPlan: getWorkspaceWithPlanFactory({ db })
|
||||
})
|
||||
getWorkspaceWithPlan: getWorkspaceWithPlanFactory({ db })
|
||||
})
|
||||
return await onWorkspaceRoleUpdated(payload)
|
||||
},
|
||||
@@ -954,9 +851,7 @@ export const initializeEventListenersFactory =
|
||||
getWorkspaceWithPlan: getWorkspaceWithPlanFactory({ db }),
|
||||
getWorkspaceRoleForUser: getWorkspaceRoleForUserFactory({ db }),
|
||||
getWorkspaceSeatTypeToProjectRoleMapping:
|
||||
getWorkspaceSeatTypeToProjectRoleMappingFactory({
|
||||
getWorkspaceWithPlan: getWorkspaceWithPlanFactory({ db })
|
||||
}),
|
||||
getWorkspaceSeatTypeToProjectRoleMappingFactory(),
|
||||
getStreamsCollaboratorCounts: getStreamsCollaboratorCountsFactory({ db }),
|
||||
getWorkspaceCollaborators: getWorkspaceCollaboratorsFactory({ db })
|
||||
})
|
||||
|
||||
@@ -249,13 +249,9 @@ const buildCollectAndValidateResourceTargets = () =>
|
||||
getWorkspaceRoleAndSeat: getWorkspaceRoleAndSeatFactory({ db }),
|
||||
getWorkspaceWithPlan: getWorkspaceWithPlanFactory({ db }),
|
||||
getWorkspaceRoleToDefaultProjectRoleMapping:
|
||||
getWorkspaceRoleToDefaultProjectRoleMappingFactory({
|
||||
getWorkspaceWithPlan: getWorkspaceWithPlanFactory({ db })
|
||||
}),
|
||||
getWorkspaceRoleToDefaultProjectRoleMappingFactory(),
|
||||
getWorkspaceSeatTypeToProjectRoleMapping:
|
||||
getWorkspaceSeatTypeToProjectRoleMappingFactory({
|
||||
getWorkspaceWithPlan: getWorkspaceWithPlanFactory({ db })
|
||||
})
|
||||
getWorkspaceSeatTypeToProjectRoleMappingFactory()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -693,17 +689,12 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
case 'teamUnlimited':
|
||||
case 'pro':
|
||||
case 'proUnlimited':
|
||||
case 'starter':
|
||||
case 'plus':
|
||||
case 'business':
|
||||
switch (workspacePlan.status) {
|
||||
case 'cancelationScheduled':
|
||||
case 'valid':
|
||||
case 'paymentFailed':
|
||||
throw new WorkspacePaidPlanActiveError()
|
||||
case 'canceled':
|
||||
case 'trial':
|
||||
case 'expired':
|
||||
break
|
||||
default:
|
||||
throwUncoveredError(workspacePlan)
|
||||
@@ -711,9 +702,6 @@ export = FF_WORKSPACES_MODULE_ENABLED
|
||||
case 'free':
|
||||
case 'unlimited':
|
||||
case 'academia':
|
||||
case 'starterInvoiced':
|
||||
case 'plusInvoiced':
|
||||
case 'businessInvoiced':
|
||||
case 'proUnlimitedInvoiced':
|
||||
case 'teamUnlimitedInvoiced':
|
||||
break
|
||||
|
||||
@@ -75,6 +75,7 @@ import {
|
||||
import { GetStream } from '@/modules/core/domain/streams/operations'
|
||||
import { GetUser } from '@/modules/core/domain/users/operations'
|
||||
import { GetWorkspaceRoleAndSeat } from '@/modules/workspacesCore/domain/operations'
|
||||
import { WorkspaceSeatType } from '@/modules/workspacesCore/domain/types'
|
||||
|
||||
export const isWorkspaceResourceTarget = (
|
||||
target: InviteResourceTarget
|
||||
@@ -154,19 +155,23 @@ export const collectAndValidateWorkspaceTargetsFactory =
|
||||
? primaryResourceTarget
|
||||
: null
|
||||
|
||||
const targetRole =
|
||||
const targetWorkspaceRole =
|
||||
primaryWorkspaceResourceTarget?.role ||
|
||||
input.primaryResourceTarget.secondaryResourceRoles?.[
|
||||
WorkspaceInviteResourceType
|
||||
] ||
|
||||
Roles.Workspace.Guest
|
||||
const targetWorkspaceSeatType =
|
||||
targetWorkspaceRole === Roles.Workspace.Admin
|
||||
? WorkspaceSeatType.Editor
|
||||
: WorkspaceSeatType.Viewer
|
||||
|
||||
// Role based checks
|
||||
if (!Object.values(Roles.Workspace).includes(targetRole)) {
|
||||
if (!Object.values(Roles.Workspace).includes(targetWorkspaceRole)) {
|
||||
throw new InviteCreateValidationError('Unexpected workspace invite role')
|
||||
}
|
||||
|
||||
if (targetRole === Roles.Workspace.Admin) {
|
||||
if (targetWorkspaceRole === Roles.Workspace.Admin) {
|
||||
const serverGuestInvite = baseTargets.find(
|
||||
(target) =>
|
||||
target.resourceType === ServerInviteResourceType &&
|
||||
@@ -224,7 +229,16 @@ export const collectAndValidateWorkspaceTargetsFactory =
|
||||
await deps.validateWorkspaceMemberProjectRoleFactory({
|
||||
workspaceId,
|
||||
userId: targetUser.id,
|
||||
projectRole
|
||||
projectRole,
|
||||
workspaceAccess: workspaceRoleAndSeat
|
||||
? {
|
||||
role: workspaceRoleAndSeat.role.role,
|
||||
seatType: workspaceRoleAndSeat.seat.type
|
||||
}
|
||||
: {
|
||||
role: targetWorkspaceRole,
|
||||
seatType: targetWorkspaceSeatType
|
||||
}
|
||||
})
|
||||
|
||||
// If project target is primary and user target is already a workspace member, mark invite as auto-acceptable
|
||||
@@ -264,7 +278,7 @@ export const collectAndValidateWorkspaceTargetsFactory =
|
||||
}
|
||||
|
||||
if (
|
||||
targetRole !== Roles.Workspace.Guest &&
|
||||
targetWorkspaceRole !== Roles.Workspace.Guest &&
|
||||
workspace.domainBasedMembershipProtectionEnabled
|
||||
) {
|
||||
const workspaceDomains = await deps.getWorkspaceDomains({
|
||||
@@ -308,7 +322,7 @@ export const collectAndValidateWorkspaceTargetsFactory =
|
||||
: {
|
||||
resourceId: workspaceId,
|
||||
resourceType: WorkspaceInviteResourceType,
|
||||
role: targetRole
|
||||
role: targetWorkspaceRole
|
||||
}
|
||||
|
||||
return [...baseTargets, finalWorkspaceResourceTarget]
|
||||
|
||||
@@ -423,8 +423,7 @@ export const updateWorkspaceRoleFactory =
|
||||
userId,
|
||||
role: nextWorkspaceRole,
|
||||
preventRoleDowngrade,
|
||||
updatedByUserId,
|
||||
skipProjectRoleUpdatesFor
|
||||
updatedByUserId
|
||||
}): Promise<void> => {
|
||||
const workspaceRoles = await getWorkspaceRoles({ workspaceId })
|
||||
|
||||
@@ -495,10 +494,7 @@ export const updateWorkspaceRoleFactory =
|
||||
workspaceId,
|
||||
role: nextWorkspaceRole
|
||||
},
|
||||
updatedByUserId,
|
||||
flags: {
|
||||
skipProjectRoleUpdatesFor: skipProjectRoleUpdatesFor ?? []
|
||||
}
|
||||
updatedByUserId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from '@/modules/workspaces/errors/workspace'
|
||||
import { GetProject, UpdateProject } from '@/modules/core/domain/projects/operations'
|
||||
import { chunk } from 'lodash'
|
||||
import { Roles, StreamRoles } from '@speckle/shared'
|
||||
import { Roles, WorkspaceRoles } from '@speckle/shared'
|
||||
import {
|
||||
GetStreamCollaborators,
|
||||
LegacyGetStreams,
|
||||
@@ -49,8 +49,6 @@ import {
|
||||
GetWorkspaceWithPlan,
|
||||
WorkspaceSeatType
|
||||
} from '@/modules/gatekeeper/domain/billing'
|
||||
import { isNewPlanType } from '@/modules/gatekeeper/helpers/plans'
|
||||
import { NotImplementedError } from '@/modules/shared/errors'
|
||||
import { FindEmailsByUserId } from '@/modules/core/domain/userEmails/operations'
|
||||
import { userEmailsCompliantWithWorkspaceDomains } from '@/modules/workspaces/domain/logic'
|
||||
import { CreateWorkspaceSeat } from '@/modules/gatekeeper/domain/operations'
|
||||
@@ -217,19 +215,7 @@ export const moveProjectToWorkspaceFactory =
|
||||
}
|
||||
|
||||
export const getWorkspaceRoleToDefaultProjectRoleMappingFactory =
|
||||
({
|
||||
getWorkspaceWithPlan
|
||||
}: {
|
||||
getWorkspaceWithPlan: GetWorkspaceWithPlan
|
||||
}): GetWorkspaceRoleToDefaultProjectRoleMapping =>
|
||||
async ({ workspaceId }) => {
|
||||
const workspace = await getWorkspaceWithPlan({ workspaceId })
|
||||
|
||||
if (!workspace) {
|
||||
throw new WorkspaceNotFoundError()
|
||||
}
|
||||
|
||||
const isNewPlan = workspace.plan && isNewPlanType(workspace.plan.name)
|
||||
(): GetWorkspaceRoleToDefaultProjectRoleMapping => async () => {
|
||||
const allowed = {
|
||||
[Roles.Workspace.Guest]: [Roles.Stream.Reviewer, Roles.Stream.Contributor],
|
||||
[Roles.Workspace.Member]: [
|
||||
@@ -244,45 +230,18 @@ export const getWorkspaceRoleToDefaultProjectRoleMappingFactory =
|
||||
]
|
||||
}
|
||||
|
||||
if (isNewPlan)
|
||||
return {
|
||||
default: {
|
||||
[Roles.Workspace.Guest]: null,
|
||||
[Roles.Workspace.Member]: null,
|
||||
[Roles.Workspace.Admin]: null
|
||||
},
|
||||
allowed
|
||||
}
|
||||
|
||||
return {
|
||||
default: {
|
||||
[Roles.Workspace.Guest]: null,
|
||||
[Roles.Workspace.Member]: Roles.Stream.Reviewer,
|
||||
[Roles.Workspace.Admin]: Roles.Stream.Owner
|
||||
[Roles.Workspace.Member]: null,
|
||||
[Roles.Workspace.Admin]: null
|
||||
},
|
||||
allowed
|
||||
}
|
||||
}
|
||||
|
||||
export const getWorkspaceSeatTypeToProjectRoleMappingFactory =
|
||||
(deps: {
|
||||
getWorkspaceWithPlan: GetWorkspaceWithPlan
|
||||
}): GetWorkspaceSeatTypeToProjectRoleMapping =>
|
||||
async (params: { workspaceId: string }) => {
|
||||
const { workspaceId } = params
|
||||
const workspace = await deps.getWorkspaceWithPlan({ workspaceId })
|
||||
|
||||
if (!workspace) {
|
||||
throw new WorkspaceNotFoundError()
|
||||
}
|
||||
|
||||
const isNewPlan = workspace.plan && isNewPlanType(workspace.plan.name)
|
||||
if (!isNewPlan) {
|
||||
throw new NotImplementedError(
|
||||
'This function is not supported for this workspace plan'
|
||||
)
|
||||
}
|
||||
|
||||
(): GetWorkspaceSeatTypeToProjectRoleMapping => async () => {
|
||||
return {
|
||||
allowed: {
|
||||
[WorkspaceSeatType.Viewer]: [Roles.Stream.Reviewer],
|
||||
@@ -310,54 +269,50 @@ export const validateWorkspaceMemberProjectRoleFactory =
|
||||
getWorkspaceSeatTypeToProjectRoleMapping: GetWorkspaceSeatTypeToProjectRoleMapping
|
||||
}): ValidateWorkspaceMemberProjectRole =>
|
||||
async (params) => {
|
||||
const { workspaceId, userId, projectRole } = params
|
||||
const { workspaceId, userId, projectRole, workspaceAccess } = params
|
||||
|
||||
const roleSeatParams = {
|
||||
workspaceId,
|
||||
userId
|
||||
let workspaceRole: WorkspaceRoles
|
||||
let seatType: WorkspaceSeatType
|
||||
|
||||
if (workspaceAccess) {
|
||||
// Check planned workspace role/seat
|
||||
workspaceRole = workspaceAccess.role
|
||||
seatType = workspaceAccess.seatType
|
||||
} else {
|
||||
// Check real workspace role/seat
|
||||
const roleSeatParams = {
|
||||
workspaceId,
|
||||
userId
|
||||
}
|
||||
|
||||
const [currentWorkspaceRoleAndSeat, workspace] = await Promise.all([
|
||||
deps.getWorkspaceRoleAndSeat(roleSeatParams),
|
||||
deps.getWorkspaceWithPlan({ workspaceId })
|
||||
])
|
||||
|
||||
if (!workspace || !currentWorkspaceRoleAndSeat?.role) return
|
||||
workspaceRole = currentWorkspaceRoleAndSeat.role.role
|
||||
seatType = currentWorkspaceRoleAndSeat.seat?.type || WorkspaceSeatType.Viewer
|
||||
}
|
||||
|
||||
const [currentWorkspaceRoleAndSeat, workspace] = await Promise.all([
|
||||
deps.getWorkspaceRoleAndSeat(roleSeatParams),
|
||||
deps.getWorkspaceWithPlan({ workspaceId })
|
||||
])
|
||||
|
||||
if (!workspace || !currentWorkspaceRoleAndSeat?.role) return
|
||||
const {
|
||||
role: { role: workspaceRole },
|
||||
seat
|
||||
} = currentWorkspaceRoleAndSeat
|
||||
const seatType = seat?.type || WorkspaceSeatType.Viewer
|
||||
|
||||
let allowedRoles: StreamRoles[]
|
||||
const isNewPlan = workspace.plan && isNewPlanType(workspace.plan.name)
|
||||
if (isNewPlan) {
|
||||
const workspaceAllowedRoles = (
|
||||
await deps.getWorkspaceRoleToDefaultProjectRoleMapping({
|
||||
workspaceId
|
||||
})
|
||||
).allowed[workspaceRole]
|
||||
const seatAllowedRoles = (
|
||||
await deps.getWorkspaceSeatTypeToProjectRoleMapping({
|
||||
workspaceId
|
||||
})
|
||||
).allowed[seatType]
|
||||
allowedRoles = Array.from(
|
||||
new Set(workspaceAllowedRoles).intersection(new Set(seatAllowedRoles))
|
||||
)
|
||||
} else {
|
||||
const roleMapping = await deps.getWorkspaceRoleToDefaultProjectRoleMapping({
|
||||
const workspaceAllowedRoles = (
|
||||
await deps.getWorkspaceRoleToDefaultProjectRoleMapping({
|
||||
workspaceId
|
||||
})
|
||||
allowedRoles = roleMapping.allowed[workspaceRole]
|
||||
}
|
||||
).allowed[workspaceRole]
|
||||
const seatAllowedRoles = (
|
||||
await deps.getWorkspaceSeatTypeToProjectRoleMapping({
|
||||
workspaceId
|
||||
})
|
||||
).allowed[seatType]
|
||||
const allowedRoles = Array.from(
|
||||
new Set(workspaceAllowedRoles).intersection(new Set(seatAllowedRoles))
|
||||
)
|
||||
|
||||
if (!allowedRoles.includes(projectRole)) {
|
||||
// User's workspace role does not allow the requested project role
|
||||
throw new WorkspaceInvalidRoleError(
|
||||
isNewPlan
|
||||
? `User's workspace seat type '${seatType}' and workspace role '${workspaceRole}' does not allow project role '${projectRole}'.`
|
||||
: `User's workspace role '${workspaceRole}' does not allow project role '${projectRole}'.`
|
||||
`User's workspace seat type '${seatType}' and workspace role '${workspaceRole}' does not allow project role '${projectRole}'.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,8 +50,11 @@ import { CreateWorkspaceInviteMutationVariables } from '@/test/graphql/generated
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import {
|
||||
MaybeNullOrUndefined,
|
||||
PaidWorkspacePlans,
|
||||
Roles,
|
||||
WorkspacePlan,
|
||||
WorkspacePlans,
|
||||
WorkspacePlanStatuses,
|
||||
WorkspaceRoles
|
||||
} from '@speckle/shared'
|
||||
import { getStreamFactory } from '@/modules/core/repositories/streams'
|
||||
@@ -70,7 +73,7 @@ import { getDefaultSsoSessionExpirationDate } from '@/modules/workspaces/domain/
|
||||
import {
|
||||
getWorkspacePlanFactory,
|
||||
getWorkspaceWithPlanFactory,
|
||||
upsertPaidWorkspacePlanFactory,
|
||||
upsertWorkspacePlanFactory,
|
||||
upsertWorkspaceSubscriptionFactory
|
||||
} from '@/modules/gatekeeper/repositories/billing'
|
||||
import { SetOptional } from 'type-fest'
|
||||
@@ -102,6 +105,7 @@ import {
|
||||
getWorkspaceSeatTypeToProjectRoleMappingFactory,
|
||||
validateWorkspaceMemberProjectRoleFactory
|
||||
} from '@/modules/workspaces/services/projects'
|
||||
import { isBoolean, isString } from 'lodash'
|
||||
import { captureCreatedInvite } from '@/test/speckle-helpers/inviteHelper'
|
||||
import {
|
||||
finalizeInvitedServerRegistrationFactory,
|
||||
@@ -141,7 +145,7 @@ export const createTestWorkspace = async (
|
||||
owner: BasicTestUser,
|
||||
options?: {
|
||||
domain?: string
|
||||
addPlan?: Pick<WorkspacePlan, 'name' | 'status'> | boolean
|
||||
addPlan?: Partial<Pick<WorkspacePlan, 'name' | 'status'>> | boolean | WorkspacePlans
|
||||
addSubscription?: boolean
|
||||
regionKey?: string
|
||||
}
|
||||
@@ -158,7 +162,7 @@ export const createTestWorkspace = async (
|
||||
return
|
||||
}
|
||||
|
||||
const upsertWorkspacePlan = upsertPaidWorkspacePlanFactory({ db })
|
||||
const upsertWorkspacePlan = upsertWorkspacePlanFactory({ db })
|
||||
const createWorkspace = createWorkspaceFactory({
|
||||
validateSlug: validateSlugFactory({
|
||||
getWorkspaceBySlug: getWorkspaceBySlugFactory({ db })
|
||||
@@ -207,21 +211,25 @@ export const createTestWorkspace = async (
|
||||
}
|
||||
|
||||
if (addPlan || useRegion) {
|
||||
let planName: WorkspacePlans
|
||||
let planStatus: WorkspacePlanStatuses
|
||||
if (isBoolean(addPlan)) {
|
||||
planName = PaidWorkspacePlans.Team
|
||||
planStatus = WorkspacePlanStatuses.Valid
|
||||
} else {
|
||||
planName = (isString(addPlan) ? addPlan : addPlan.name) || PaidWorkspacePlans.Team
|
||||
planStatus =
|
||||
(isString(addPlan) ? WorkspacePlanStatuses.Valid : addPlan.status) ||
|
||||
WorkspacePlanStatuses.Valid
|
||||
}
|
||||
|
||||
await upsertWorkspacePlan({
|
||||
workspacePlan: {
|
||||
createdAt: new Date(),
|
||||
workspaceId: newWorkspace.id,
|
||||
name:
|
||||
typeof addPlan === 'object' && Object.hasOwn(addPlan, 'name')
|
||||
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(addPlan.name as any)
|
||||
: 'business',
|
||||
status:
|
||||
typeof addPlan === 'object' && Object.hasOwn(addPlan, 'status')
|
||||
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(addPlan.status as any)
|
||||
: 'valid'
|
||||
}
|
||||
name: planName,
|
||||
status: planStatus
|
||||
} as WorkspacePlan
|
||||
})
|
||||
}
|
||||
|
||||
@@ -411,13 +419,9 @@ export const createWorkspaceInviteDirectly = async (
|
||||
getWorkspaceRoleAndSeat: getWorkspaceRoleAndSeatFactory({ db }),
|
||||
getWorkspaceWithPlan: getWorkspaceWithPlanFactory({ db }),
|
||||
getWorkspaceRoleToDefaultProjectRoleMapping:
|
||||
getWorkspaceRoleToDefaultProjectRoleMappingFactory({
|
||||
getWorkspaceWithPlan: getWorkspaceWithPlanFactory({ db })
|
||||
}),
|
||||
getWorkspaceRoleToDefaultProjectRoleMappingFactory(),
|
||||
getWorkspaceSeatTypeToProjectRoleMapping:
|
||||
getWorkspaceSeatTypeToProjectRoleMappingFactory({
|
||||
getWorkspaceWithPlan: getWorkspaceWithPlanFactory({ db })
|
||||
})
|
||||
getWorkspaceSeatTypeToProjectRoleMappingFactory()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -12,8 +12,12 @@ import {
|
||||
CreateWorkspaceProjectInviteDocument,
|
||||
CreateWorkspaceProjectInviteMutationVariables,
|
||||
GetMyWorkspaceInvitesDocument,
|
||||
GetProjectDocument,
|
||||
GetProjectQueryVariables,
|
||||
GetWorkspaceDocument,
|
||||
GetWorkspaceInviteDocument,
|
||||
GetWorkspaceInviteQueryVariables,
|
||||
GetWorkspaceQueryVariables,
|
||||
GetWorkspaceWithTeamDocument,
|
||||
GetWorkspaceWithTeamQueryVariables,
|
||||
ResendWorkspaceInviteDocument,
|
||||
@@ -27,16 +31,20 @@ import { expect } from 'chai'
|
||||
|
||||
import { MaybeAsync, StreamRoles, WorkspaceRoles } from '@speckle/shared'
|
||||
import { expectToThrow } from '@/test/assertionHelper'
|
||||
|
||||
import { getWorkspaceFactory } from '@/modules/workspaces/repositories/workspaces'
|
||||
|
||||
import { ForbiddenError } from '@/modules/shared/errors'
|
||||
import { getStreamFactory } from '@/modules/core/repositories/streams'
|
||||
import { db } from '@/db/knex'
|
||||
|
||||
export const buildInvitesGraphqlOperations = (deps: { apollo: TestApolloServer }) => {
|
||||
const { apollo } = deps
|
||||
const getStream = getStreamFactory({ db })
|
||||
|
||||
const getWorkspace = async (
|
||||
args: GetWorkspaceQueryVariables,
|
||||
options?: ExecuteOperationOptions
|
||||
) => apollo.execute(GetWorkspaceDocument, args, options)
|
||||
|
||||
const getProject = async (
|
||||
args: GetProjectQueryVariables,
|
||||
options?: ExecuteOperationOptions
|
||||
) => apollo.execute(GetProjectDocument, args, options)
|
||||
|
||||
const useInvite = async (
|
||||
args: UseWorkspaceInviteMutationVariables,
|
||||
@@ -91,7 +99,8 @@ export const buildInvitesGraphqlOperations = (deps: { apollo: TestApolloServer }
|
||||
}
|
||||
|
||||
await wrapAccessCheck(async () => {
|
||||
const workspace = await getWorkspaceFactory({ db })({ workspaceId, userId })
|
||||
const res = await getWorkspace({ workspaceId }, { authUserId: userId })
|
||||
const workspace = res.data?.workspace
|
||||
if (!workspace?.role) {
|
||||
throw new ForbiddenError('Missing workspace role')
|
||||
}
|
||||
@@ -108,14 +117,20 @@ export const buildInvitesGraphqlOperations = (deps: { apollo: TestApolloServer }
|
||||
|
||||
if (streamId?.length) {
|
||||
await wrapAccessCheck(async () => {
|
||||
const project = await getStream({ streamId, userId })
|
||||
if (!project?.role) {
|
||||
const res = await getProject({ id: streamId }, { authUserId: userId })
|
||||
const project = res.data?.project
|
||||
|
||||
// No need to check for project role, since it can be implicit from workspace
|
||||
if (!project?.id) {
|
||||
throw new ForbiddenError('Missing project role')
|
||||
}
|
||||
|
||||
if (params.expectedProjectRole && project.role !== params.expectedProjectRole) {
|
||||
if (
|
||||
params.expectedProjectRole &&
|
||||
project?.role !== params.expectedProjectRole
|
||||
) {
|
||||
throw new ForbiddenError(
|
||||
`Unexpected project role! Expected: ${params.expectedProjectRole}, real: ${project.role}`
|
||||
`Unexpected project role! Expected: ${params.expectedProjectRole}, real: ${project?.role}`
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -66,6 +66,7 @@ import {
|
||||
buildInvitesGraphqlOperations
|
||||
} from '@/modules/workspaces/tests/helpers/invites'
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { WorkspaceSeatType } from '@/modules/workspacesCore/domain/types'
|
||||
|
||||
enum InviteByTarget {
|
||||
Email = 'email',
|
||||
@@ -528,7 +529,12 @@ describe('Workspaces Invites GQL', () => {
|
||||
]
|
||||
])
|
||||
await assignToWorkspaces([
|
||||
[myProjectInviteTargetWorkspace, myWorkspaceFriend, Roles.Workspace.Member],
|
||||
[
|
||||
myProjectInviteTargetWorkspace,
|
||||
myWorkspaceFriend,
|
||||
Roles.Workspace.Member,
|
||||
WorkspaceSeatType.Editor
|
||||
],
|
||||
[
|
||||
myProjectInviteTargetWorkspace,
|
||||
workspaceMemberWithNoProjectAccess,
|
||||
@@ -615,7 +621,7 @@ describe('Workspaces Invites GQL', () => {
|
||||
inputs: [
|
||||
{
|
||||
userId: otherGuy.id,
|
||||
role: Roles.Stream.Owner
|
||||
role: Roles.Stream.Reviewer
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -671,7 +677,7 @@ describe('Workspaces Invites GQL', () => {
|
||||
inputs: [
|
||||
{
|
||||
userId: workspaceMemberWithNoProjectAccess.id,
|
||||
role: Roles.Stream.Owner
|
||||
role: Roles.Stream.Reviewer
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1026,7 +1032,7 @@ describe('Workspaces Invites GQL', () => {
|
||||
inputs: [
|
||||
{
|
||||
userId: otherGuy.id,
|
||||
role: Roles.Stream.Owner
|
||||
role: Roles.Stream.Reviewer
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1506,7 +1512,7 @@ describe('Workspaces Invites GQL', () => {
|
||||
await validateResourceAccess({
|
||||
shouldHaveAccess: true,
|
||||
expectedWorkspaceRole: Roles.Workspace.Guest,
|
||||
expectedProjectRole: Roles.Stream.Owner
|
||||
expectedProjectRole: Roles.Stream.Reviewer
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1525,7 +1531,7 @@ describe('Workspaces Invites GQL', () => {
|
||||
inputs: [
|
||||
{
|
||||
userId: otherGuy.id,
|
||||
role: Roles.Stream.Owner,
|
||||
role: withRole ? Roles.Stream.Owner : Roles.Stream.Reviewer,
|
||||
workspaceRole: withRole ? Roles.Workspace.Admin : undefined
|
||||
}
|
||||
]
|
||||
@@ -1562,7 +1568,7 @@ describe('Workspaces Invites GQL', () => {
|
||||
expectedWorkspaceRole: withRole
|
||||
? Roles.Workspace.Admin
|
||||
: Roles.Workspace.Guest,
|
||||
expectedProjectRole: Roles.Stream.Owner
|
||||
expectedProjectRole: withRole ? Roles.Stream.Owner : Roles.Stream.Reviewer
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -36,7 +36,13 @@ import {
|
||||
createTestStream,
|
||||
getUserStreamRole
|
||||
} from '@/test/speckle-helpers/streamHelper'
|
||||
import { isNonNullable, Nullable, Optional, Roles } from '@speckle/shared'
|
||||
import {
|
||||
isNonNullable,
|
||||
Nullable,
|
||||
Optional,
|
||||
PaidWorkspacePlans,
|
||||
Roles
|
||||
} from '@speckle/shared'
|
||||
import { expect } from 'chai'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import dayjs from 'dayjs'
|
||||
@@ -118,137 +124,120 @@ describe('Workspace project GQL CRUD', () => {
|
||||
])
|
||||
})
|
||||
|
||||
describeEach(
|
||||
[{ oldPlan: true }, { oldPlan: false }],
|
||||
({ oldPlan }) => `with ${oldPlan ? 'old (business)' : 'new (pro)'} plan`,
|
||||
({ oldPlan }) => {
|
||||
const roleProject: BasicTestStream = {
|
||||
name: 'Role Project',
|
||||
isPublic: false,
|
||||
id: '',
|
||||
ownerId: ''
|
||||
}
|
||||
describe(`with pro plan`, () => {
|
||||
const roleProject: BasicTestStream = {
|
||||
name: 'Role Project',
|
||||
isPublic: false,
|
||||
id: '',
|
||||
ownerId: ''
|
||||
}
|
||||
|
||||
const roleWorkspace: BasicTestWorkspace = {
|
||||
id: '',
|
||||
ownerId: '',
|
||||
slug: cryptoRandomString({ length: 10 }),
|
||||
name: 'Role Workspace'
|
||||
}
|
||||
const roleWorkspace: BasicTestWorkspace = {
|
||||
id: '',
|
||||
ownerId: '',
|
||||
slug: cryptoRandomString({ length: 10 }),
|
||||
name: 'Role Workspace'
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
// TODO: Multiregion
|
||||
await createTestWorkspace(roleWorkspace, serverAdminUser, {
|
||||
addPlan: oldPlan
|
||||
? { name: 'business', status: 'valid' }
|
||||
: { name: 'pro', status: 'valid' }
|
||||
})
|
||||
roleProject.workspaceId = roleWorkspace.id
|
||||
before(async () => {
|
||||
// TODO: Multiregion
|
||||
await createTestWorkspace(roleWorkspace, serverAdminUser, {
|
||||
addPlan: { name: PaidWorkspacePlans.Pro, status: 'valid' }
|
||||
})
|
||||
roleProject.workspaceId = roleWorkspace.id
|
||||
|
||||
await Promise.all([
|
||||
assignToWorkspace(roleWorkspace, workspaceGuest, Roles.Workspace.Guest),
|
||||
assignToWorkspace(
|
||||
roleWorkspace,
|
||||
workspaceEditor,
|
||||
Roles.Workspace.Member,
|
||||
WorkspaceSeatType.Editor
|
||||
),
|
||||
assignToWorkspace(
|
||||
roleWorkspace,
|
||||
workspaceMemberViewer,
|
||||
Roles.Workspace.Member,
|
||||
WorkspaceSeatType.Viewer
|
||||
)
|
||||
])
|
||||
await createTestStream(roleProject, serverAdminUser)
|
||||
|
||||
await Promise.all([
|
||||
addToStream(roleProject, workspaceGuest, Roles.Stream.Reviewer),
|
||||
addToStream(roleProject, workspaceEditor, Roles.Stream.Contributor),
|
||||
addToStream(roleProject, workspaceMemberViewer, Roles.Stream.Reviewer)
|
||||
])
|
||||
|
||||
// assert seat types
|
||||
const seats = await getWorkspaceUserSeatsFactory({ db })({
|
||||
workspaceId: roleWorkspace.id,
|
||||
userIds: [workspaceGuest.id, workspaceEditor.id, workspaceMemberViewer.id]
|
||||
})
|
||||
expect(seats[workspaceGuest.id].type).to.equal(WorkspaceSeatType.Viewer)
|
||||
expect(seats[workspaceEditor.id].type).to.equal(WorkspaceSeatType.Editor)
|
||||
expect(seats[workspaceMemberViewer.id].type).to.equal(
|
||||
await Promise.all([
|
||||
assignToWorkspace(roleWorkspace, workspaceGuest, Roles.Workspace.Guest),
|
||||
assignToWorkspace(
|
||||
roleWorkspace,
|
||||
workspaceEditor,
|
||||
Roles.Workspace.Member,
|
||||
WorkspaceSeatType.Editor
|
||||
),
|
||||
assignToWorkspace(
|
||||
roleWorkspace,
|
||||
workspaceMemberViewer,
|
||||
Roles.Workspace.Member,
|
||||
WorkspaceSeatType.Viewer
|
||||
)
|
||||
])
|
||||
await createTestStream(roleProject, serverAdminUser)
|
||||
|
||||
await Promise.all([
|
||||
addToStream(roleProject, workspaceGuest, Roles.Stream.Reviewer),
|
||||
addToStream(roleProject, workspaceEditor, Roles.Stream.Contributor),
|
||||
addToStream(roleProject, workspaceMemberViewer, Roles.Stream.Reviewer)
|
||||
])
|
||||
|
||||
// assert seat types
|
||||
const seats = await getWorkspaceUserSeatsFactory({ db })({
|
||||
workspaceId: roleWorkspace.id,
|
||||
userIds: [workspaceGuest.id, workspaceEditor.id, workspaceMemberViewer.id]
|
||||
})
|
||||
expect(seats[workspaceGuest.id].type).to.equal(WorkspaceSeatType.Viewer)
|
||||
expect(seats[workspaceEditor.id].type).to.equal(WorkspaceSeatType.Editor)
|
||||
expect(seats[workspaceMemberViewer.id].type).to.equal(WorkspaceSeatType.Viewer)
|
||||
})
|
||||
|
||||
describeEach(
|
||||
[{ oldResolver: true }, { oldResolver: false }],
|
||||
({ oldResolver }) =>
|
||||
`with ${oldResolver ? 'old' : 'new'} updateRole resolver`,
|
||||
({ oldResolver }) => {
|
||||
const updateRole = async (input: ProjectUpdateRoleInput) => {
|
||||
if (oldResolver) {
|
||||
const res = await apollo.execute(UpdateProjectRoleDocument, {
|
||||
input
|
||||
})
|
||||
const project = res.data?.projectMutations?.updateRole
|
||||
return { res, project }
|
||||
} else {
|
||||
const res = await apollo.execute(UpdateWorkspaceProjectRoleDocument, {
|
||||
input
|
||||
})
|
||||
const project = res.data?.workspaceMutations?.projects?.updateRole
|
||||
return { res, project }
|
||||
}
|
||||
describeEach(
|
||||
[{ oldResolver: true }, { oldResolver: false }],
|
||||
({ oldResolver }) => `with ${oldResolver ? 'old' : 'new'} updateRole resolver`,
|
||||
({ oldResolver }) => {
|
||||
const updateRole = async (input: ProjectUpdateRoleInput) => {
|
||||
if (oldResolver) {
|
||||
const res = await apollo.execute(UpdateProjectRoleDocument, {
|
||||
input
|
||||
})
|
||||
const project = res.data?.projectMutations?.updateRole
|
||||
return { res, project }
|
||||
} else {
|
||||
const res = await apollo.execute(UpdateWorkspaceProjectRoleDocument, {
|
||||
input
|
||||
})
|
||||
const project = res.data?.workspaceMutations?.projects?.updateRole
|
||||
return { res, project }
|
||||
}
|
||||
|
||||
it("can't set a workspace guest as a project owner", async () => {
|
||||
const { res } = await updateRole({
|
||||
projectId: roleProject.id,
|
||||
userId: workspaceGuest.id,
|
||||
role: Roles.Stream.Owner
|
||||
})
|
||||
const newRole = await getUserStreamRole(workspaceGuest.id, roleProject.id)
|
||||
|
||||
expect(res).to.haveGraphQLErrors({ code: WorkspaceInvalidRoleError.code })
|
||||
expect(newRole).to.eq(Roles.Stream.Reviewer)
|
||||
})
|
||||
|
||||
it(`can${
|
||||
oldPlan ? '' : 'not'
|
||||
} set a workspace viewer as a project contributor or owner`, async () => {
|
||||
const { res: resA } = await updateRole({
|
||||
projectId: roleProject.id,
|
||||
userId: workspaceMemberViewer.id,
|
||||
role: Roles.Stream.Contributor
|
||||
})
|
||||
const { res: resB } = await updateRole({
|
||||
projectId: roleProject.id,
|
||||
userId: workspaceMemberViewer.id,
|
||||
role: Roles.Stream.Owner
|
||||
})
|
||||
const newRole = await getUserStreamRole(
|
||||
workspaceMemberViewer.id,
|
||||
roleProject.id
|
||||
)
|
||||
|
||||
if (oldPlan) {
|
||||
expect(resA).to.not.haveGraphQLErrors()
|
||||
expect(resB).to.not.haveGraphQLErrors()
|
||||
expect(newRole).to.eq(Roles.Stream.Owner)
|
||||
} else {
|
||||
expect(resA).to.haveGraphQLErrors({
|
||||
code: WorkspaceInvalidRoleError.code
|
||||
})
|
||||
expect(resB).to.haveGraphQLErrors({
|
||||
code: WorkspaceInvalidRoleError.code
|
||||
})
|
||||
expect(newRole).to.eq(Roles.Stream.Reviewer)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
it("can't set a workspace guest as a project owner", async () => {
|
||||
const { res } = await updateRole({
|
||||
projectId: roleProject.id,
|
||||
userId: workspaceGuest.id,
|
||||
role: Roles.Stream.Owner
|
||||
})
|
||||
const newRole = await getUserStreamRole(workspaceGuest.id, roleProject.id)
|
||||
|
||||
expect(res).to.haveGraphQLErrors({ code: WorkspaceInvalidRoleError.code })
|
||||
expect(newRole).to.eq(Roles.Stream.Reviewer)
|
||||
})
|
||||
|
||||
it(`can not set a workspace viewer as a project contributor or owner`, async () => {
|
||||
const { res: resA } = await updateRole({
|
||||
projectId: roleProject.id,
|
||||
userId: workspaceMemberViewer.id,
|
||||
role: Roles.Stream.Contributor
|
||||
})
|
||||
const { res: resB } = await updateRole({
|
||||
projectId: roleProject.id,
|
||||
userId: workspaceMemberViewer.id,
|
||||
role: Roles.Stream.Owner
|
||||
})
|
||||
const newRole = await getUserStreamRole(
|
||||
workspaceMemberViewer.id,
|
||||
roleProject.id
|
||||
)
|
||||
|
||||
expect(resA).to.haveGraphQLErrors({
|
||||
code: WorkspaceInvalidRoleError.code
|
||||
})
|
||||
expect(resB).to.haveGraphQLErrors({
|
||||
code: WorkspaceInvalidRoleError.code
|
||||
})
|
||||
expect(newRole).to.eq(Roles.Stream.Reviewer)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when specifying a workspace id during project creation', () => {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { testApolloServer, TestApolloServer } from '@/test/graphqlHelper'
|
||||
import { beforeEachContext, getRegionKeys } from '@/test/hooks'
|
||||
import { MultiRegionDbSelectorMock } from '@/test/mocks/global'
|
||||
import { truncateRegionsSafely } from '@/test/speckle-helpers/regions'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { PaidWorkspacePlans, Roles } from '@speckle/shared'
|
||||
import { expect } from 'chai'
|
||||
|
||||
const storeRegion = storeRegionFactory({ db })
|
||||
@@ -51,7 +51,10 @@ isEnabled
|
||||
|
||||
await Promise.all([
|
||||
// Create first test workspace
|
||||
createTestWorkspace(myFirstWorkspace, me),
|
||||
createTestWorkspace(myFirstWorkspace, me, {
|
||||
// pro for custom regions
|
||||
addPlan: { name: PaidWorkspacePlans.Pro }
|
||||
}),
|
||||
// Create a couple of test regions
|
||||
storeRegion({
|
||||
region: {
|
||||
@@ -91,7 +94,28 @@ isEnabled
|
||||
})
|
||||
})
|
||||
|
||||
describe('when setting default region', () => {
|
||||
it("can't set default region on invalid plan", async () => {
|
||||
const workspace: BasicTestWorkspace = {
|
||||
id: '',
|
||||
ownerId: '',
|
||||
slug: '',
|
||||
name: 'My second workspace'
|
||||
}
|
||||
await createTestWorkspace(workspace, me, {
|
||||
addPlan: { name: PaidWorkspacePlans.Team }
|
||||
})
|
||||
|
||||
const res = await apollo.execute(
|
||||
SetWorkspaceDefaultRegionDocument,
|
||||
{ workspaceId: workspace.id, regionKey: region1Key },
|
||||
{ authUserId: me.id }
|
||||
)
|
||||
|
||||
expect(res).to.haveGraphQLErrors('Specified region not available for workspace')
|
||||
expect(res.data?.workspaceMutations.setDefaultRegion).to.be.not.ok
|
||||
})
|
||||
|
||||
describe('when setting default region on valid plan', () => {
|
||||
const mySecondWorkspace: BasicTestWorkspace = {
|
||||
id: '',
|
||||
ownerId: '',
|
||||
@@ -100,7 +124,10 @@ isEnabled
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
await createTestWorkspace(mySecondWorkspace, me)
|
||||
await createTestWorkspace(mySecondWorkspace, me, {
|
||||
// pro for custom regions
|
||||
addPlan: { name: PaidWorkspacePlans.Pro }
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -145,7 +172,10 @@ isEnabled
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
await createTestWorkspace(myThirdWorkspace, me)
|
||||
await createTestWorkspace(myThirdWorkspace, me, {
|
||||
// pro for custom regions
|
||||
addPlan: { name: PaidWorkspacePlans.Pro }
|
||||
})
|
||||
await apollo.execute(
|
||||
SetWorkspaceDefaultRegionDocument,
|
||||
{
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
waitForRegionUsers
|
||||
} from '@/test/speckle-helpers/regions'
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { WorkspacePlans } from '@speckle/shared'
|
||||
import { expect } from 'chai'
|
||||
|
||||
enum WorkspaceIdentification {
|
||||
@@ -104,7 +105,8 @@ describe('Workspace GQL Subscriptions', () => {
|
||||
before(async () => {
|
||||
await waitForRegionUsers([me, otherGuy])
|
||||
await createTestWorkspace(myMainWorkspace, me, {
|
||||
regionKey: isMultiRegion ? getMainTestRegionKey() : undefined
|
||||
regionKey: isMultiRegion ? getMainTestRegionKey() : undefined,
|
||||
addPlan: WorkspacePlans.Pro
|
||||
})
|
||||
})
|
||||
|
||||
@@ -194,7 +196,8 @@ describe('Workspace GQL Subscriptions', () => {
|
||||
|
||||
before(async () => {
|
||||
await createTestWorkspace(myTeamWorkspace, me, {
|
||||
regionKey: isMultiRegion ? getMainTestRegionKey() : undefined
|
||||
regionKey: isMultiRegion ? getMainTestRegionKey() : undefined,
|
||||
addPlan: WorkspacePlans.Pro
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
import { Workspace, WorkspaceAcl } from '@/modules/workspacesCore/domain/types'
|
||||
import { Roles } from '@speckle/shared'
|
||||
import { StreamAclRecord, StreamRecord } from '@/modules/core/helpers/types'
|
||||
import { onProjectCreatedFactory } from '@/modules/workspaces/events/eventListener'
|
||||
import { expect } from 'chai'
|
||||
import { GetWorkspaceRolesAndSeats } from '@/modules/gatekeeper/domain/billing'
|
||||
|
||||
describe('Event handlers', () => {
|
||||
describe('onProjectCreatedFactory creates a function, that', () => {
|
||||
it('grants project roles for all workspace members, except guests', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const projectId = cryptoRandomString({ length: 10 })
|
||||
|
||||
const workspaceRoles: WorkspaceAcl[] = [
|
||||
{
|
||||
workspaceId,
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
role: Roles.Workspace.Admin,
|
||||
createdAt: new Date()
|
||||
},
|
||||
{
|
||||
workspaceId,
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
role: Roles.Workspace.Member,
|
||||
createdAt: new Date()
|
||||
},
|
||||
{
|
||||
workspaceId,
|
||||
userId: cryptoRandomString({ length: 10 }),
|
||||
role: Roles.Workspace.Guest,
|
||||
createdAt: new Date()
|
||||
}
|
||||
]
|
||||
|
||||
const projectRoles: StreamAclRecord[] = []
|
||||
|
||||
const onProjectCreated = onProjectCreatedFactory({
|
||||
getWorkspaceRolesAndSeats: async () =>
|
||||
workspaceRoles.reduce((acc, role) => {
|
||||
acc[role.userId] = { role, seat: null, userId: role.userId }
|
||||
return acc
|
||||
}, {} as Awaited<ReturnType<GetWorkspaceRolesAndSeats>>),
|
||||
getWorkspaceWithPlan: async () =>
|
||||
({
|
||||
id: workspaceId
|
||||
} as Workspace & { plan: null }),
|
||||
getWorkspaceRoleToDefaultProjectRoleMapping: async () => ({
|
||||
default: {
|
||||
[Roles.Workspace.Admin]: Roles.Stream.Owner,
|
||||
[Roles.Workspace.Member]: Roles.Stream.Contributor,
|
||||
[Roles.Workspace.Guest]: null
|
||||
},
|
||||
allowed: {
|
||||
[Roles.Workspace.Admin]: [
|
||||
Roles.Stream.Owner,
|
||||
Roles.Stream.Contributor,
|
||||
Roles.Stream.Reviewer
|
||||
],
|
||||
[Roles.Workspace.Member]: [
|
||||
Roles.Stream.Owner,
|
||||
Roles.Stream.Contributor,
|
||||
Roles.Stream.Reviewer
|
||||
],
|
||||
[Roles.Workspace.Guest]: [Roles.Stream.Reviewer, Roles.Stream.Contributor]
|
||||
}
|
||||
}),
|
||||
upsertProjectRole: async ({ projectId, userId, role }) => {
|
||||
projectRoles.push({
|
||||
resourceId: projectId,
|
||||
userId,
|
||||
role
|
||||
})
|
||||
|
||||
return {} as StreamRecord
|
||||
}
|
||||
})
|
||||
|
||||
await onProjectCreated({
|
||||
project: { workspaceId, id: projectId } as StreamRecord,
|
||||
ownerId: cryptoRandomString({ length: 10 }),
|
||||
input: { name: 'test' }
|
||||
})
|
||||
|
||||
expect(projectRoles.length).to.equal(2)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -834,10 +834,7 @@ describe('Workspace role services', () => {
|
||||
expect(context.eventData.eventName).to.equal(WorkspaceEvents.RoleUpdated)
|
||||
expect(payload).to.deep.equal({
|
||||
acl: role,
|
||||
updatedByUserId: workspaceOwnerId,
|
||||
flags: {
|
||||
skipProjectRoleUpdatesFor: []
|
||||
}
|
||||
updatedByUserId: workspaceOwnerId
|
||||
})
|
||||
})
|
||||
it('throws if attempting to remove the last admin in a workspace', async () => {
|
||||
|
||||
@@ -93,14 +93,24 @@ describe('Project retrieval services', () => {
|
||||
|
||||
describe('Project management services', () => {
|
||||
describe('moveProjectToWorkspaceFactory returns a function, that', () => {
|
||||
const roleMapping: [
|
||||
StreamRoles, // Current project role
|
||||
WorkspaceRoles | null, // Current workspace role
|
||||
WorkspaceSeatType | null, // Current workspace seat type
|
||||
StreamRoles, // Final project role
|
||||
WorkspaceRoles, // Final workspace role
|
||||
WorkspaceSeatType // Final workspace seat type
|
||||
][] = [
|
||||
const roleMapping: Array<
|
||||
| [
|
||||
StreamRoles, // Current project role
|
||||
null, // Current workspace role
|
||||
null, // Current workspace seat type
|
||||
StreamRoles, // Final project role
|
||||
WorkspaceRoles, // Final workspace role
|
||||
WorkspaceSeatType // Final workspace seat type
|
||||
]
|
||||
| [
|
||||
StreamRoles, // Current project role
|
||||
WorkspaceRoles, // Current workspace role
|
||||
WorkspaceSeatType, // Current workspace seat type
|
||||
StreamRoles, // Final project role
|
||||
WorkspaceRoles, // Final workspace role
|
||||
WorkspaceSeatType // Final workspace seat type
|
||||
]
|
||||
> = [
|
||||
[
|
||||
Roles.Stream.Owner,
|
||||
Roles.Workspace.Admin,
|
||||
@@ -343,7 +353,13 @@ describe('Project management services', () => {
|
||||
workspaceId,
|
||||
createdAt: new Date()
|
||||
},
|
||||
seat: null,
|
||||
seat: {
|
||||
workspaceId,
|
||||
userId,
|
||||
type: WorkspaceSeatType.Editor,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
},
|
||||
userId
|
||||
}
|
||||
}
|
||||
@@ -471,7 +487,7 @@ describe('Project management services', () => {
|
||||
]
|
||||
},
|
||||
getWorkspaceRolesAndSeats: async () => {
|
||||
return workspaceRole
|
||||
return workspaceRole && workspaceSeatType
|
||||
? {
|
||||
[userId]: {
|
||||
role: {
|
||||
@@ -480,15 +496,13 @@ describe('Project management services', () => {
|
||||
workspaceId,
|
||||
createdAt: new Date()
|
||||
},
|
||||
seat: workspaceSeatType
|
||||
? {
|
||||
workspaceId,
|
||||
userId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
type: workspaceSeatType
|
||||
}
|
||||
: null,
|
||||
seat: {
|
||||
workspaceId,
|
||||
userId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
type: workspaceSeatType
|
||||
},
|
||||
userId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { WorkspaceAcl, WorkspaceSeat } from '@/modules/workspacesCore/domain/types'
|
||||
import { Nullable } from '@speckle/shared'
|
||||
|
||||
export type GetWorkspaceRolesAndSeats = (params: {
|
||||
workspaceId: string
|
||||
@@ -7,7 +6,7 @@ export type GetWorkspaceRolesAndSeats = (params: {
|
||||
}) => Promise<{
|
||||
[userId: string]: {
|
||||
role: WorkspaceAcl
|
||||
seat: Nullable<WorkspaceSeat>
|
||||
seat: WorkspaceSeat
|
||||
userId: string
|
||||
}
|
||||
}>
|
||||
@@ -18,7 +17,7 @@ export type GetWorkspaceRoleAndSeat = (params: {
|
||||
}) => Promise<
|
||||
| {
|
||||
role: WorkspaceAcl
|
||||
seat: Nullable<WorkspaceSeat>
|
||||
seat: WorkspaceSeat
|
||||
userId: string
|
||||
}
|
||||
| undefined
|
||||
|
||||
@@ -46,7 +46,7 @@ export const getWorkspaceRolesAndSeatsFactory =
|
||||
|
||||
acc[role.userId] = {
|
||||
role,
|
||||
seat: formatJsonArrayRecords(row.seats || [])[0] || null,
|
||||
seat: formatJsonArrayRecords(row.seats || [])[0],
|
||||
userId: role.userId
|
||||
}
|
||||
return acc
|
||||
|
||||
@@ -1923,11 +1923,8 @@ export type OnboardingCompletionInput = {
|
||||
};
|
||||
|
||||
export const PaidWorkspacePlans = {
|
||||
Business: 'business',
|
||||
Plus: 'plus',
|
||||
Pro: 'pro',
|
||||
ProUnlimited: 'proUnlimited',
|
||||
Starter: 'starter',
|
||||
Team: 'team',
|
||||
TeamUnlimited: 'teamUnlimited'
|
||||
} as const;
|
||||
@@ -4882,9 +4879,7 @@ export type WorkspacePlanPrice = {
|
||||
export const WorkspacePlanStatuses = {
|
||||
CancelationScheduled: 'cancelationScheduled',
|
||||
Canceled: 'canceled',
|
||||
Expired: 'expired',
|
||||
PaymentFailed: 'paymentFailed',
|
||||
Trial: 'trial',
|
||||
Valid: 'valid'
|
||||
} as const;
|
||||
|
||||
@@ -4897,16 +4892,10 @@ export type WorkspacePlanUsage = {
|
||||
|
||||
export const WorkspacePlans = {
|
||||
Academia: 'academia',
|
||||
Business: 'business',
|
||||
BusinessInvoiced: 'businessInvoiced',
|
||||
Free: 'free',
|
||||
Plus: 'plus',
|
||||
PlusInvoiced: 'plusInvoiced',
|
||||
Pro: 'pro',
|
||||
ProUnlimited: 'proUnlimited',
|
||||
ProUnlimitedInvoiced: 'proUnlimitedInvoiced',
|
||||
Starter: 'starter',
|
||||
StarterInvoiced: 'starterInvoiced',
|
||||
Team: 'team',
|
||||
TeamUnlimited: 'teamUnlimited',
|
||||
TeamUnlimitedInvoiced: 'teamUnlimitedInvoiced',
|
||||
@@ -5888,7 +5877,7 @@ export type GetProjectQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type GetProjectQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, name: string, workspaceId?: string | null } };
|
||||
export type GetProjectQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, name: string, workspaceId?: string | null, role?: string | null, description?: string | null, visibility: SimpleProjectVisibility, allowPublicComments: boolean, createdAt: string, updatedAt: string } };
|
||||
|
||||
export type CreateProjectMutationVariables = Exact<{
|
||||
input: ProjectCreateInput;
|
||||
@@ -6171,7 +6160,7 @@ export type MarkProjectVersionReceivedMutationVariables = Exact<{
|
||||
|
||||
export type MarkProjectVersionReceivedMutation = { __typename?: 'Mutation', versionMutations: { __typename?: 'VersionMutations', markReceived: boolean } };
|
||||
|
||||
export type TestWorkspaceFragment = { __typename?: 'Workspace', id: string, name: string, slug: string, description?: string | null, createdAt: string, updatedAt: string, logo?: string | null, readOnly: boolean, discoverabilityEnabled: boolean };
|
||||
export type TestWorkspaceFragment = { __typename?: 'Workspace', id: string, name: string, slug: string, description?: string | null, createdAt: string, updatedAt: string, logo?: string | null, readOnly: boolean, discoverabilityEnabled: boolean, role?: string | null };
|
||||
|
||||
export type TestWorkspaceCollaboratorFragment = { __typename?: 'WorkspaceCollaborator', id: string, role: string, user: { __typename?: 'LimitedUser', name: string }, projectRoles: Array<{ __typename?: 'ProjectRole', role: string, project: { __typename?: 'Project', id: string, name: string } }> };
|
||||
|
||||
@@ -6182,7 +6171,7 @@ export type CreateWorkspaceMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateWorkspaceMutation = { __typename?: 'Mutation', workspaceMutations: { __typename?: 'WorkspaceMutations', create: { __typename?: 'Workspace', id: string, name: string, slug: string, description?: string | null, createdAt: string, updatedAt: string, logo?: string | null, readOnly: boolean, discoverabilityEnabled: boolean } } };
|
||||
export type CreateWorkspaceMutation = { __typename?: 'Mutation', workspaceMutations: { __typename?: 'WorkspaceMutations', create: { __typename?: 'Workspace', id: string, name: string, slug: string, description?: string | null, createdAt: string, updatedAt: string, logo?: string | null, readOnly: boolean, discoverabilityEnabled: boolean, role?: string | null } } };
|
||||
|
||||
export type DeleteWorkspaceMutationVariables = Exact<{
|
||||
workspaceId: Scalars['String']['input'];
|
||||
@@ -6196,14 +6185,14 @@ export type GetWorkspaceQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type GetWorkspaceQuery = { __typename?: 'Query', workspace: { __typename?: 'Workspace', id: string, name: string, slug: string, description?: string | null, createdAt: string, updatedAt: string, logo?: string | null, readOnly: boolean, discoverabilityEnabled: boolean, team: { __typename?: 'WorkspaceCollaboratorCollection', items: Array<{ __typename?: 'WorkspaceCollaborator', id: string, role: string, user: { __typename?: 'LimitedUser', name: string }, projectRoles: Array<{ __typename?: 'ProjectRole', role: string, project: { __typename?: 'Project', id: string, name: string } }> }> } } };
|
||||
export type GetWorkspaceQuery = { __typename?: 'Query', workspace: { __typename?: 'Workspace', id: string, name: string, slug: string, description?: string | null, createdAt: string, updatedAt: string, logo?: string | null, readOnly: boolean, discoverabilityEnabled: boolean, role?: string | null, team: { __typename?: 'WorkspaceCollaboratorCollection', items: Array<{ __typename?: 'WorkspaceCollaborator', id: string, role: string, user: { __typename?: 'LimitedUser', name: string }, projectRoles: Array<{ __typename?: 'ProjectRole', role: string, project: { __typename?: 'Project', id: string, name: string } }> }> } } };
|
||||
|
||||
export type GetWorkspaceBySlugQueryVariables = Exact<{
|
||||
workspaceSlug: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetWorkspaceBySlugQuery = { __typename?: 'Query', workspaceBySlug: { __typename?: 'Workspace', id: string, name: string, slug: string, description?: string | null, createdAt: string, updatedAt: string, logo?: string | null, readOnly: boolean, discoverabilityEnabled: boolean, team: { __typename?: 'WorkspaceCollaboratorCollection', items: Array<{ __typename?: 'WorkspaceCollaborator', id: string, role: string, user: { __typename?: 'LimitedUser', name: string }, projectRoles: Array<{ __typename?: 'ProjectRole', role: string, project: { __typename?: 'Project', id: string, name: string } }> }> } } };
|
||||
export type GetWorkspaceBySlugQuery = { __typename?: 'Query', workspaceBySlug: { __typename?: 'Workspace', id: string, name: string, slug: string, description?: string | null, createdAt: string, updatedAt: string, logo?: string | null, readOnly: boolean, discoverabilityEnabled: boolean, role?: string | null, team: { __typename?: 'WorkspaceCollaboratorCollection', items: Array<{ __typename?: 'WorkspaceCollaborator', id: string, role: string, user: { __typename?: 'LimitedUser', name: string }, projectRoles: Array<{ __typename?: 'ProjectRole', role: string, project: { __typename?: 'Project', id: string, name: string } }> }> } } };
|
||||
|
||||
export type GetActiveUserDiscoverableWorkspacesQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
@@ -6215,12 +6204,12 @@ export type UpdateWorkspaceMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdateWorkspaceMutation = { __typename?: 'Mutation', workspaceMutations: { __typename?: 'WorkspaceMutations', update: { __typename?: 'Workspace', id: string, name: string, slug: string, description?: string | null, createdAt: string, updatedAt: string, logo?: string | null, readOnly: boolean, discoverabilityEnabled: boolean } } };
|
||||
export type UpdateWorkspaceMutation = { __typename?: 'Mutation', workspaceMutations: { __typename?: 'WorkspaceMutations', update: { __typename?: 'Workspace', id: string, name: string, slug: string, description?: string | null, createdAt: string, updatedAt: string, logo?: string | null, readOnly: boolean, discoverabilityEnabled: boolean, role?: string | null } } };
|
||||
|
||||
export type GetActiveUserWorkspacesQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetActiveUserWorkspacesQuery = { __typename?: 'Query', activeUser?: { __typename?: 'User', workspaces: { __typename?: 'WorkspaceCollection', items: Array<{ __typename?: 'Workspace', id: string, name: string, slug: string, description?: string | null, createdAt: string, updatedAt: string, logo?: string | null, readOnly: boolean, discoverabilityEnabled: boolean }> } } | null };
|
||||
export type GetActiveUserWorkspacesQuery = { __typename?: 'Query', activeUser?: { __typename?: 'User', workspaces: { __typename?: 'WorkspaceCollection', items: Array<{ __typename?: 'Workspace', id: string, name: string, slug: string, description?: string | null, createdAt: string, updatedAt: string, logo?: string | null, readOnly: boolean, discoverabilityEnabled: boolean, role?: string | null }> } } | null };
|
||||
|
||||
export type UpdateWorkspaceRoleMutationVariables = Exact<{
|
||||
input: WorkspaceRoleUpdateInput;
|
||||
@@ -6311,7 +6300,7 @@ export const BasicStreamFieldsFragmentDoc = {"kind":"Document","definitions":[{"
|
||||
export const UserWithEmailsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UserWithEmails"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"emails"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}},{"kind":"Field","name":{"kind":"Name","value":"primary"}}]}}]}}]} as unknown as DocumentNode<UserWithEmailsFragment, unknown>;
|
||||
export const BaseUserFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BaseUserFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"bio"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]} as unknown as DocumentNode<BaseUserFieldsFragment, unknown>;
|
||||
export const BaseLimitedUserFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BaseLimitedUserFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedUser"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"bio"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"verified"}}]}}]} as unknown as DocumentNode<BaseLimitedUserFieldsFragment, unknown>;
|
||||
export const TestWorkspaceFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}},{"kind":"Field","name":{"kind":"Name","value":"discoverabilityEnabled"}}]}}]} as unknown as DocumentNode<TestWorkspaceFragment, unknown>;
|
||||
export const TestWorkspaceFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}},{"kind":"Field","name":{"kind":"Name","value":"discoverabilityEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]} as unknown as DocumentNode<TestWorkspaceFragment, unknown>;
|
||||
export const TestWorkspaceCollaboratorFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"projectRoles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"project"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode<TestWorkspaceCollaboratorFragment, unknown>;
|
||||
export const TestWorkspaceProjectFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspaceProject"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]} as unknown as DocumentNode<TestWorkspaceProjectFragment, unknown>;
|
||||
export const CreateObjectDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateObject"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ObjectCreateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"objectCreate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"objectInput"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<CreateObjectMutation, CreateObjectMutationVariables>;
|
||||
@@ -6414,7 +6403,7 @@ export const CreateProjectCommentReplyDocument = {"kind":"Document","definitions
|
||||
export const EditProjectCommentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"EditProjectComment"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EditCommentInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"commentMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edit"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicProjectComment"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicProjectComment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rawText"}},{"kind":"Field","name":{"kind":"Name","value":"text"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"doc"}}]}},{"kind":"Field","name":{"kind":"Name","value":"authorId"}}]}}]} as unknown as DocumentNode<EditProjectCommentMutation, EditProjectCommentMutationVariables>;
|
||||
export const AdminProjectListDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"AdminProjectList"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"query"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"visibility"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},"defaultValue":{"kind":"IntValue","value":"25"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}},"defaultValue":{"kind":"NullValue"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"admin"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"query"},"value":{"kind":"Variable","name":{"kind":"Name","value":"query"}}},{"kind":"Argument","name":{"kind":"Name","value":"orderBy"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderBy"}}},{"kind":"Argument","name":{"kind":"Name","value":"visibility"},"value":{"kind":"Variable","name":{"kind":"Name","value":"visibility"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicProjectFields"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicProjectFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"allowPublicComments"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<AdminProjectListQuery, AdminProjectListQueryVariables>;
|
||||
export const GetProjectObjectDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProjectObject"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"objectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"object"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"objectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode<GetProjectObjectQuery, GetProjectObjectQueryVariables>;
|
||||
export const GetProjectDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProject"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}}]}}]}}]} as unknown as DocumentNode<GetProjectQuery, GetProjectQueryVariables>;
|
||||
export const GetProjectDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProject"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicProjectFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicProjectFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"allowPublicComments"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<GetProjectQuery, GetProjectQueryVariables>;
|
||||
export const CreateProjectDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateProject"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectCreateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicProjectFields"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicProjectFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"allowPublicComments"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<CreateProjectMutation, CreateProjectMutationVariables>;
|
||||
export const BatchDeleteProjectsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"BatchDeleteProjects"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"ids"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"batchDelete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"ids"},"value":{"kind":"Variable","name":{"kind":"Name","value":"ids"}}}]}]}}]}}]} as unknown as DocumentNode<BatchDeleteProjectsMutation, BatchDeleteProjectsMutationVariables>;
|
||||
export const UpdateProjectRoleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateProjectRole"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectUpdateRoleInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateRole"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicProjectFields"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicProjectFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"allowPublicComments"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<UpdateProjectRoleMutation, UpdateProjectRoleMutationVariables>;
|
||||
@@ -6454,13 +6443,13 @@ export const UserActiveResourcesDocument = {"kind":"Document","definitions":[{"k
|
||||
export const SetUserActiveWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetUserActiveWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"slug"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"isProjectsActive"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUserMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setActiveWorkspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"slug"}}},{"kind":"Argument","name":{"kind":"Name","value":"isProjectsActive"},"value":{"kind":"Variable","name":{"kind":"Name","value":"isProjectsActive"}}}]}]}}]}}]} as unknown as DocumentNode<SetUserActiveWorkspaceMutation, SetUserActiveWorkspaceMutationVariables>;
|
||||
export const CreateProjectVersionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateProjectVersion"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateVersionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"versionMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"sourceApplication"}},{"kind":"Field","name":{"kind":"Name","value":"model"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}}]}}]}}]}}]} as unknown as DocumentNode<CreateProjectVersionMutation, CreateProjectVersionMutationVariables>;
|
||||
export const MarkProjectVersionReceivedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"MarkProjectVersionReceived"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"MarkReceivedVersionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"versionMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"markReceived"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]}}]} as unknown as DocumentNode<MarkProjectVersionReceivedMutation, MarkProjectVersionReceivedMutationVariables>;
|
||||
export const CreateWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceCreateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspace"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}},{"kind":"Field","name":{"kind":"Name","value":"discoverabilityEnabled"}}]}}]} as unknown as DocumentNode<CreateWorkspaceMutation, CreateWorkspaceMutationVariables>;
|
||||
export const CreateWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceCreateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspace"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}},{"kind":"Field","name":{"kind":"Name","value":"discoverabilityEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]} as unknown as DocumentNode<CreateWorkspaceMutation, CreateWorkspaceMutationVariables>;
|
||||
export const DeleteWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"delete"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}]}]}}]}}]} as unknown as DocumentNode<DeleteWorkspaceMutation, DeleteWorkspaceMutationVariables>;
|
||||
export const GetWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspaceCollaborator"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}},{"kind":"Field","name":{"kind":"Name","value":"discoverabilityEnabled"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"projectRoles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"project"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode<GetWorkspaceQuery, GetWorkspaceQueryVariables>;
|
||||
export const GetWorkspaceBySlugDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspaceBySlug"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceBySlug"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspaceCollaborator"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}},{"kind":"Field","name":{"kind":"Name","value":"discoverabilityEnabled"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"projectRoles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"project"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode<GetWorkspaceBySlugQuery, GetWorkspaceBySlugQueryVariables>;
|
||||
export const GetWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspaceCollaborator"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}},{"kind":"Field","name":{"kind":"Name","value":"discoverabilityEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"projectRoles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"project"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode<GetWorkspaceQuery, GetWorkspaceQueryVariables>;
|
||||
export const GetWorkspaceBySlugDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspaceBySlug"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceSlug"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceBySlug"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"slug"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceSlug"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspaceCollaborator"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}},{"kind":"Field","name":{"kind":"Name","value":"discoverabilityEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"projectRoles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"project"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode<GetWorkspaceBySlugQuery, GetWorkspaceBySlugQueryVariables>;
|
||||
export const GetActiveUserDiscoverableWorkspacesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"getActiveUserDiscoverableWorkspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"discoverableWorkspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}}]}}]}}]}}]}}]} as unknown as DocumentNode<GetActiveUserDiscoverableWorkspacesQuery, GetActiveUserDiscoverableWorkspacesQueryVariables>;
|
||||
export const UpdateWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceUpdateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"update"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspace"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}},{"kind":"Field","name":{"kind":"Name","value":"discoverabilityEnabled"}}]}}]} as unknown as DocumentNode<UpdateWorkspaceMutation, UpdateWorkspaceMutationVariables>;
|
||||
export const GetActiveUserWorkspacesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetActiveUserWorkspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspace"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}},{"kind":"Field","name":{"kind":"Name","value":"discoverabilityEnabled"}}]}}]} as unknown as DocumentNode<GetActiveUserWorkspacesQuery, GetActiveUserWorkspacesQueryVariables>;
|
||||
export const UpdateWorkspaceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateWorkspace"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceUpdateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"update"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspace"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}},{"kind":"Field","name":{"kind":"Name","value":"discoverabilityEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]} as unknown as DocumentNode<UpdateWorkspaceMutation, UpdateWorkspaceMutationVariables>;
|
||||
export const GetActiveUserWorkspacesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetActiveUserWorkspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaces"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspace"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}},{"kind":"Field","name":{"kind":"Name","value":"discoverabilityEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]} as unknown as DocumentNode<GetActiveUserWorkspacesQuery, GetActiveUserWorkspacesQueryVariables>;
|
||||
export const UpdateWorkspaceRoleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateWorkspaceRole"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceRoleUpdateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateRole"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspaceCollaborator"}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"projectRoles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"project"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode<UpdateWorkspaceRoleMutation, UpdateWorkspaceRoleMutationVariables>;
|
||||
export const CreateWorkspaceProjectDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateWorkspaceProject"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceProjectCreateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projects"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspaceProject"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspaceProject"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]} as unknown as DocumentNode<CreateWorkspaceProjectMutation, CreateWorkspaceProjectMutationVariables>;
|
||||
export const GetWorkspaceProjectsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspaceProjects"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceProjectsFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"TestWorkspaceProject"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"TestWorkspaceProject"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]} as unknown as DocumentNode<GetWorkspaceProjectsQuery, GetWorkspaceProjectsQueryVariables>;
|
||||
|
||||
@@ -58,8 +58,12 @@ export const getProjectQuery = gql`
|
||||
id
|
||||
name
|
||||
workspaceId
|
||||
role
|
||||
...BasicProjectFields
|
||||
}
|
||||
}
|
||||
|
||||
${basicProjectFieldsFragment}
|
||||
`
|
||||
|
||||
export const createProjectMutation = gql`
|
||||
|
||||
@@ -11,6 +11,7 @@ export const workspaceFragment = gql`
|
||||
logo
|
||||
readOnly
|
||||
discoverabilityEnabled
|
||||
role
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -22,10 +22,7 @@ import {
|
||||
ProjectContext,
|
||||
WorkspaceContext
|
||||
} from '../domain/context.js'
|
||||
import {
|
||||
isNewWorkspacePlan,
|
||||
isWorkspacePlanStatusReadOnly
|
||||
} from '../../workspaces/helpers/plans.js'
|
||||
import { isWorkspacePlanStatusReadOnly } from '../../workspaces/helpers/plans.js'
|
||||
import { hasEditorSeat } from '../checks/workspaceSeat.js'
|
||||
|
||||
/**
|
||||
@@ -182,13 +179,11 @@ export const ensureWorkspaceProjectCanBeCreatedFragment: AuthPolicyEnsureFragmen
|
||||
|
||||
// Now check editor seat
|
||||
if (userId) {
|
||||
if (isNewWorkspacePlan(workspacePlan.name)) {
|
||||
const isEditor = await hasEditorSeat(loaders)({
|
||||
userId,
|
||||
workspaceId
|
||||
})
|
||||
if (!isEditor) return err(new WorkspaceNoEditorSeatError())
|
||||
}
|
||||
const isEditor = await hasEditorSeat(loaders)({
|
||||
userId,
|
||||
workspaceId
|
||||
})
|
||||
if (!isEditor) return err(new WorkspaceNoEditorSeatError())
|
||||
}
|
||||
|
||||
const workspaceLimits = await loaders.getWorkspaceLimits({ workspaceId })
|
||||
|
||||
@@ -391,7 +391,7 @@ describe('canCreateWorkspaceProjectPolicy creates a function, that handles', ()
|
||||
},
|
||||
getWorkspacePlan: async () => {
|
||||
return {
|
||||
status: 'expired'
|
||||
status: 'canceled'
|
||||
} as WorkspacePlan
|
||||
},
|
||||
getWorkspaceLimits: async () => {
|
||||
@@ -428,7 +428,7 @@ describe('canCreateWorkspaceProjectPolicy creates a function, that handles', ()
|
||||
return {} as Workspace
|
||||
},
|
||||
getWorkspaceSeat: async () => {
|
||||
return 'viewer'
|
||||
return 'editor'
|
||||
},
|
||||
getWorkspacePlan: async () => {
|
||||
return {
|
||||
@@ -466,7 +466,7 @@ describe('canCreateWorkspaceProjectPolicy creates a function, that handles', ()
|
||||
return {} as Workspace
|
||||
},
|
||||
getWorkspaceSeat: async () => {
|
||||
return 'viewer'
|
||||
return 'editor'
|
||||
},
|
||||
getWorkspacePlan: async () => {
|
||||
return {
|
||||
@@ -507,7 +507,7 @@ describe('canCreateWorkspaceProjectPolicy creates a function, that handles', ()
|
||||
return {} as Workspace
|
||||
},
|
||||
getWorkspaceSeat: async () => {
|
||||
return 'viewer'
|
||||
return 'editor'
|
||||
},
|
||||
getWorkspacePlan: async () => {
|
||||
return {
|
||||
@@ -550,7 +550,7 @@ describe('canCreateWorkspaceProjectPolicy creates a function, that handles', ()
|
||||
return {} as Workspace
|
||||
},
|
||||
getWorkspaceSeat: async () => {
|
||||
return 'viewer'
|
||||
return 'editor'
|
||||
},
|
||||
getWorkspacePlan: async () => {
|
||||
return {
|
||||
@@ -591,7 +591,7 @@ describe('canCreateWorkspaceProjectPolicy creates a function, that handles', ()
|
||||
return {} as Workspace
|
||||
},
|
||||
getWorkspaceSeat: async () => {
|
||||
return 'viewer'
|
||||
return 'editor'
|
||||
},
|
||||
getWorkspacePlan: async () => {
|
||||
return {
|
||||
|
||||
@@ -86,26 +86,6 @@ const baseFeatures = [
|
||||
export const WorkspacePaidPlanConfigs: {
|
||||
[plan in PaidWorkspacePlans]: WorkspacePlanConfig<plan>
|
||||
} = {
|
||||
// Old
|
||||
[PaidWorkspacePlans.Starter]: {
|
||||
plan: PaidWorkspacePlans.Starter,
|
||||
features: [...baseFeatures],
|
||||
limits: unlimited
|
||||
},
|
||||
[PaidWorkspacePlans.Plus]: {
|
||||
plan: PaidWorkspacePlans.Plus,
|
||||
features: [...baseFeatures, WorkspacePlanFeatures.SSO],
|
||||
limits: unlimited
|
||||
},
|
||||
[PaidWorkspacePlans.Business]: {
|
||||
plan: PaidWorkspacePlans.Business,
|
||||
features: [
|
||||
...baseFeatures,
|
||||
WorkspacePlanFeatures.SSO,
|
||||
WorkspacePlanFeatures.CustomDataRegion
|
||||
],
|
||||
limits: unlimited
|
||||
},
|
||||
[PaidWorkspacePlans.Team]: {
|
||||
plan: PaidWorkspacePlans.Team,
|
||||
features: [...baseFeatures],
|
||||
@@ -116,7 +96,6 @@ export const WorkspacePaidPlanConfigs: {
|
||||
commentHistory: { value: 30, unit: 'day' }
|
||||
}
|
||||
},
|
||||
// New
|
||||
[PaidWorkspacePlans.TeamUnlimited]: {
|
||||
plan: PaidWorkspacePlans.TeamUnlimited,
|
||||
features: [...baseFeatures],
|
||||
@@ -162,7 +141,6 @@ export const WorkspacePaidPlanConfigs: {
|
||||
export const WorkspaceUnpaidPlanConfigs: {
|
||||
[plan in UnpaidWorkspacePlans]: WorkspacePlanConfig<plan>
|
||||
} = {
|
||||
// Old
|
||||
[UnpaidWorkspacePlans.Unlimited]: {
|
||||
plan: UnpaidWorkspacePlans.Unlimited,
|
||||
features: [
|
||||
@@ -183,19 +161,6 @@ export const WorkspaceUnpaidPlanConfigs: {
|
||||
],
|
||||
limits: unlimited
|
||||
},
|
||||
[UnpaidWorkspacePlans.StarterInvoiced]: {
|
||||
...WorkspacePaidPlanConfigs.starter,
|
||||
plan: UnpaidWorkspacePlans.StarterInvoiced
|
||||
},
|
||||
[UnpaidWorkspacePlans.PlusInvoiced]: {
|
||||
...WorkspacePaidPlanConfigs.plus,
|
||||
plan: UnpaidWorkspacePlans.PlusInvoiced
|
||||
},
|
||||
[UnpaidWorkspacePlans.BusinessInvoiced]: {
|
||||
...WorkspacePaidPlanConfigs.business,
|
||||
plan: UnpaidWorkspacePlans.BusinessInvoiced
|
||||
},
|
||||
// New
|
||||
[UnpaidWorkspacePlans.TeamUnlimitedInvoiced]: {
|
||||
...WorkspacePaidPlanConfigs.teamUnlimited,
|
||||
plan: UnpaidWorkspacePlans.TeamUnlimitedInvoiced
|
||||
|
||||
@@ -1,48 +1,15 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
doesPlanIncludeUnlimitedProjectsAddon,
|
||||
isNewWorkspacePlan,
|
||||
isSelfServeAvailablePlan,
|
||||
WorkspacePlans
|
||||
} from './plans.js'
|
||||
|
||||
describe('plan helpers', () => {
|
||||
describe('isNewWorkspacePlan', () => {
|
||||
const planCases: {
|
||||
[P in WorkspacePlans]: boolean
|
||||
} = <const>{
|
||||
business: false,
|
||||
businessInvoiced: false,
|
||||
plus: false,
|
||||
plusInvoiced: false,
|
||||
starter: false,
|
||||
starterInvoiced: false,
|
||||
free: true,
|
||||
academia: true,
|
||||
unlimited: true,
|
||||
pro: true,
|
||||
proUnlimited: true,
|
||||
proUnlimitedInvoiced: true,
|
||||
team: true,
|
||||
teamUnlimited: true,
|
||||
teamUnlimitedInvoiced: true
|
||||
}
|
||||
it.each(Object.entries(planCases))('plan %s is new type -> %s', (plan, isNew) => {
|
||||
const result = isNewWorkspacePlan(plan as WorkspacePlans)
|
||||
expect(result).toStrictEqual(isNew)
|
||||
})
|
||||
})
|
||||
|
||||
describe('doesPlanIncludeUnlimitedProjectsAddon', () => {
|
||||
const planCases: {
|
||||
[P in WorkspacePlans]: boolean
|
||||
} = <const>{
|
||||
business: false,
|
||||
businessInvoiced: false,
|
||||
plus: false,
|
||||
plusInvoiced: false,
|
||||
starter: false,
|
||||
starterInvoiced: false,
|
||||
free: false,
|
||||
academia: false,
|
||||
unlimited: false,
|
||||
@@ -66,12 +33,6 @@ describe('plan helpers', () => {
|
||||
const planCases: {
|
||||
[P in WorkspacePlans]: boolean
|
||||
} = <const>{
|
||||
business: false,
|
||||
businessInvoiced: false,
|
||||
plus: false,
|
||||
plusInvoiced: false,
|
||||
starter: false,
|
||||
starterInvoiced: false,
|
||||
free: true,
|
||||
academia: false,
|
||||
unlimited: false,
|
||||
|
||||
@@ -1,50 +1,16 @@
|
||||
import { throwUncoveredError } from '../../core/helpers/error.js'
|
||||
import type { MaybeNullOrUndefined } from '../../core/helpers/utilityTypes.js'
|
||||
|
||||
/**
|
||||
* PLANS
|
||||
*/
|
||||
|
||||
export const TrialEnabledPaidWorkspacePlans = <const>{
|
||||
Starter: 'starter'
|
||||
}
|
||||
|
||||
export type TrialEnabledPaidWorkspacePlans =
|
||||
(typeof TrialEnabledPaidWorkspacePlans)[keyof typeof TrialEnabledPaidWorkspacePlans]
|
||||
|
||||
export const PaidWorkspacePlansOld = <const>{
|
||||
...TrialEnabledPaidWorkspacePlans,
|
||||
Plus: 'plus',
|
||||
Business: 'business'
|
||||
}
|
||||
|
||||
export type PaidWorkspacePlansOld =
|
||||
(typeof PaidWorkspacePlansOld)[keyof typeof PaidWorkspacePlansOld]
|
||||
|
||||
export const PaidWorkspacePlansNew = <const>{
|
||||
Team: 'team',
|
||||
TeamUnlimited: 'teamUnlimited',
|
||||
Pro: 'pro',
|
||||
ProUnlimited: 'proUnlimited'
|
||||
}
|
||||
|
||||
export type PaidWorkspacePlansNew =
|
||||
(typeof PaidWorkspacePlansNew)[keyof typeof PaidWorkspacePlansNew]
|
||||
|
||||
export const PaidWorkspacePlans = <const>{
|
||||
...PaidWorkspacePlansOld,
|
||||
...PaidWorkspacePlansNew
|
||||
Team: 'team', // actually 'Starter'
|
||||
TeamUnlimited: 'teamUnlimited',
|
||||
Pro: 'pro', // actually 'Business'
|
||||
ProUnlimited: 'proUnlimited'
|
||||
}
|
||||
|
||||
export type PaidWorkspacePlans =
|
||||
(typeof PaidWorkspacePlans)[keyof typeof PaidWorkspacePlans]
|
||||
|
||||
export const UnpaidWorkspacePlans = <const>{
|
||||
// Old
|
||||
StarterInvoiced: 'starterInvoiced',
|
||||
PlusInvoiced: 'plusInvoiced',
|
||||
BusinessInvoiced: 'businessInvoiced',
|
||||
// New
|
||||
TeamUnlimitedInvoiced: 'teamUnlimitedInvoiced',
|
||||
ProUnlimitedInvoiced: 'proUnlimitedInvoiced',
|
||||
Unlimited: 'unlimited',
|
||||
@@ -62,38 +28,6 @@ export const WorkspacePlans = <const>{
|
||||
|
||||
export type WorkspacePlans = (typeof WorkspacePlans)[keyof typeof WorkspacePlans]
|
||||
|
||||
// TODO: Remove this post workspace migration
|
||||
export const WorkspaceGuestSeatType = 'guest'
|
||||
export type WorkspaceGuestSeatType = typeof WorkspaceGuestSeatType
|
||||
|
||||
// TODO: Remove this post workspace migration, only needed temporarily to differiante between old and new
|
||||
export const isNewWorkspacePlan = (
|
||||
plan: MaybeNullOrUndefined<WorkspacePlans>
|
||||
): boolean => {
|
||||
if (!plan) return false
|
||||
switch (plan) {
|
||||
case 'starter':
|
||||
case 'starterInvoiced':
|
||||
case 'plus':
|
||||
case 'plusInvoiced':
|
||||
case 'business':
|
||||
case 'businessInvoiced':
|
||||
return false
|
||||
case 'team':
|
||||
case 'teamUnlimited':
|
||||
case 'teamUnlimitedInvoiced':
|
||||
case 'pro':
|
||||
case 'proUnlimited':
|
||||
case 'proUnlimitedInvoiced':
|
||||
case 'unlimited':
|
||||
case 'academia':
|
||||
case 'free':
|
||||
return true
|
||||
default:
|
||||
throwUncoveredError(plan)
|
||||
}
|
||||
}
|
||||
|
||||
export const doesPlanIncludeUnlimitedProjectsAddon = (
|
||||
plan: WorkspacePlans
|
||||
): boolean => {
|
||||
@@ -104,12 +38,6 @@ export const doesPlanIncludeUnlimitedProjectsAddon = (
|
||||
case 'free':
|
||||
case 'team':
|
||||
case 'pro':
|
||||
case 'starter':
|
||||
case 'plus':
|
||||
case 'business':
|
||||
case 'starterInvoiced':
|
||||
case 'plusInvoiced':
|
||||
case 'businessInvoiced':
|
||||
case 'teamUnlimitedInvoiced':
|
||||
case 'proUnlimitedInvoiced':
|
||||
case 'unlimited':
|
||||
@@ -129,12 +57,6 @@ export const isSelfServeAvailablePlan = (plan: WorkspacePlans): boolean => {
|
||||
case 'pro':
|
||||
case 'proUnlimited':
|
||||
return true
|
||||
case 'starter':
|
||||
case 'plus':
|
||||
case 'business':
|
||||
case 'starterInvoiced':
|
||||
case 'plusInvoiced':
|
||||
case 'businessInvoiced':
|
||||
case 'teamUnlimitedInvoiced':
|
||||
case 'proUnlimitedInvoiced':
|
||||
case 'unlimited':
|
||||
@@ -154,12 +76,6 @@ export const isPaidPlan = (plan: WorkspacePlans): boolean => {
|
||||
case 'proUnlimited':
|
||||
return true
|
||||
case 'free':
|
||||
case 'starter':
|
||||
case 'plus':
|
||||
case 'business':
|
||||
case 'starterInvoiced':
|
||||
case 'plusInvoiced':
|
||||
case 'businessInvoiced':
|
||||
case 'teamUnlimitedInvoiced':
|
||||
case 'proUnlimitedInvoiced':
|
||||
case 'unlimited':
|
||||
@@ -204,17 +120,8 @@ export const PaidWorkspacePlanStatuses = <const>{
|
||||
export type PaidWorkspacePlanStatuses =
|
||||
(typeof PaidWorkspacePlanStatuses)[keyof typeof PaidWorkspacePlanStatuses]
|
||||
|
||||
export const TrialWorkspacePlanStatuses = <const>{
|
||||
Trial: 'trial',
|
||||
Expired: 'expired'
|
||||
}
|
||||
|
||||
export type TrialWorkspacePlanStatuses =
|
||||
(typeof TrialWorkspacePlanStatuses)[keyof typeof TrialWorkspacePlanStatuses]
|
||||
|
||||
export const WorkspacePlanStatuses = <const>{
|
||||
...PaidWorkspacePlanStatuses,
|
||||
...TrialWorkspacePlanStatuses,
|
||||
...UnpaidWorkspacePlanStatuses
|
||||
}
|
||||
|
||||
@@ -231,25 +138,18 @@ export type PaidWorkspacePlan = BaseWorkspacePlan & {
|
||||
status: PaidWorkspacePlanStatuses
|
||||
}
|
||||
|
||||
export type TrialWorkspacePlan = BaseWorkspacePlan & {
|
||||
name: TrialEnabledPaidWorkspacePlans
|
||||
status: TrialWorkspacePlanStatuses
|
||||
}
|
||||
|
||||
export type UnpaidWorkspacePlan = BaseWorkspacePlan & {
|
||||
name: UnpaidWorkspacePlans
|
||||
status: UnpaidWorkspacePlanStatuses
|
||||
}
|
||||
export type WorkspacePlan = PaidWorkspacePlan | TrialWorkspacePlan | UnpaidWorkspacePlan
|
||||
export type WorkspacePlan = PaidWorkspacePlan | UnpaidWorkspacePlan
|
||||
|
||||
export const isWorkspacePlanStatusReadOnly = (status: WorkspacePlan['status']) => {
|
||||
switch (status) {
|
||||
case 'cancelationScheduled':
|
||||
case 'valid':
|
||||
case 'trial':
|
||||
case 'paymentFailed':
|
||||
return false
|
||||
case 'expired':
|
||||
case 'canceled':
|
||||
return true
|
||||
default:
|
||||
|
||||
@@ -614,42 +614,6 @@ Generate the environment variables for Speckle server and Speckle objects deploy
|
||||
name: "{{ default .Values.secretName .Values.billing.secretName }}"
|
||||
key: {{ .Values.billing.stripeEndpointSigningKey.secretKey }}
|
||||
|
||||
- name: WORKSPACE_GUEST_SEAT_STRIPE_PRODUCT_ID
|
||||
value: {{ .Values.billing.workspaceGuestSeatStripeProductId }}
|
||||
|
||||
- name: WORKSPACE_MONTHLY_GUEST_SEAT_STRIPE_PRICE_ID
|
||||
value: {{ .Values.billing.workspaceMonthlyGuestSeatStripePriceId }}
|
||||
|
||||
- name: WORKSPACE_YEARLY_GUEST_SEAT_STRIPE_PRICE_ID
|
||||
value: {{ .Values.billing.workspaceYearlyGuestSeatStripePriceId }}
|
||||
|
||||
- name: WORKSPACE_STARTER_SEAT_STRIPE_PRODUCT_ID
|
||||
value: {{ .Values.billing.workspaceStarterSeatStripeProductId }}
|
||||
|
||||
- name: WORKSPACE_MONTHLY_STARTER_SEAT_STRIPE_PRICE_ID
|
||||
value: {{ .Values.billing.workspaceMonthlyStarterSeatStripePriceId }}
|
||||
|
||||
- name: WORKSPACE_YEARLY_STARTER_SEAT_STRIPE_PRICE_ID
|
||||
value: {{ .Values.billing.workspaceYearlyStarterSeatStripePriceId }}
|
||||
|
||||
- name: WORKSPACE_PLUS_SEAT_STRIPE_PRODUCT_ID
|
||||
value: {{ .Values.billing.workspacePlusSeatStripeProductId }}
|
||||
|
||||
- name: WORKSPACE_MONTHLY_PLUS_SEAT_STRIPE_PRICE_ID
|
||||
value: {{ .Values.billing.workspaceMonthlyPlusSeatStripePriceId }}
|
||||
|
||||
- name: WORKSPACE_YEARLY_PLUS_SEAT_STRIPE_PRICE_ID
|
||||
value: {{ .Values.billing.workspaceYearlyPlusSeatStripePriceId }}
|
||||
|
||||
- name: WORKSPACE_BUSINESS_SEAT_STRIPE_PRODUCT_ID
|
||||
value: {{ .Values.billing.workspaceBusinessSeatStripeProductId }}
|
||||
|
||||
- name: WORKSPACE_MONTHLY_BUSINESS_SEAT_STRIPE_PRICE_ID
|
||||
value: {{ .Values.billing.workspaceMonthlyBusinessSeatStripePriceId }}
|
||||
|
||||
- name: WORKSPACE_YEARLY_BUSINESS_SEAT_STRIPE_PRICE_ID
|
||||
value: {{ .Values.billing.workspaceYearlyBusinessSeatStripePriceId }}
|
||||
|
||||
- name: WORKSPACE_TEAM_SEAT_STRIPE_PRODUCT_ID
|
||||
value: {{ .Values.billing.workspaceTeamSeatStripeProductId }}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user