feat(gatekeeper): new checkout flow
This commit is contained in:
@@ -59,3 +59,9 @@ export class WorkspaceReadOnlyError extends BaseError {
|
||||
static code = 'WORKSPACE_READ_ONLY_ERROR'
|
||||
static statusCode = 403
|
||||
}
|
||||
|
||||
export class InvalidWorkspacePlanUpgradeError extends BaseError {
|
||||
static defaultMessage = 'Cannot upgrade to the specified workspace plan'
|
||||
static code = 'INVALID_WORKSPACE_PLAN_UPGRADE_ERROR'
|
||||
static statusCode = 403
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
import { WorkspaceNotFoundError } from '@/modules/workspaces/errors/workspace'
|
||||
import { db } from '@/db/knex'
|
||||
import {
|
||||
createCheckoutSessionFactory,
|
||||
createCustomerPortalUrlFactory,
|
||||
reconcileWorkspaceSubscriptionFactory
|
||||
} from '@/modules/gatekeeper/clients/stripe'
|
||||
@@ -18,7 +17,6 @@ import {
|
||||
getStripeClient,
|
||||
getWorkspacePlanProductId
|
||||
} from '@/modules/gatekeeper/stripe'
|
||||
import { startCheckoutSessionFactory } from '@/modules/gatekeeper/services/checkout'
|
||||
import {
|
||||
deleteCheckoutSessionFactory,
|
||||
getWorkspaceCheckoutSessionFactory,
|
||||
@@ -31,19 +29,37 @@ import {
|
||||
import { canWorkspaceAccessFeatureFactory } from '@/modules/gatekeeper/services/featureAuthorization'
|
||||
import { upgradeWorkspaceSubscriptionFactory } from '@/modules/gatekeeper/services/subscriptions'
|
||||
import { isWorkspaceReadOnlyFactory } from '@/modules/gatekeeper/services/readOnly'
|
||||
import { calculateSubscriptionSeats } from '@/modules/gatekeeper/domain/billing'
|
||||
import {
|
||||
calculateSubscriptionSeats,
|
||||
CreateCheckoutSession,
|
||||
CreateCheckoutSessionOld
|
||||
} from '@/modules/gatekeeper/domain/billing'
|
||||
import { WorkspacePaymentMethod } from '@/test/graphql/generated/graphql'
|
||||
import { LogicError, NotImplementedError } from '@/modules/shared/errors'
|
||||
import { isNewPlanType } from '@/modules/gatekeeper/helpers/plans'
|
||||
import { extendLoggerComponent } from '@/observability/logging'
|
||||
import { OperationName, OperationStatus } from '@/observability/domain/fields'
|
||||
import { logWithErr } from '@/observability/utils/logLevels'
|
||||
import {
|
||||
createCheckoutSessionFactoryNew,
|
||||
createCheckoutSessionFactoryOld
|
||||
} from '@/modules/gatekeeper/clients/checkout/createCheckoutSession'
|
||||
import {
|
||||
startCheckoutSessionFactoryNew,
|
||||
startCheckoutSessionFactoryOld
|
||||
} from '@/modules/gatekeeper/services/checkout/startCheckoutSession'
|
||||
import { countSeatsByTypeInWorkspaceFactory } from '@/modules/gatekeeper/repositories/workspaceSeat'
|
||||
|
||||
const { FF_GATEKEEPER_MODULE_ENABLED, FF_BILLING_INTEGRATION_ENABLED } =
|
||||
getFeatureFlags()
|
||||
|
||||
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: {
|
||||
@@ -140,7 +156,7 @@ export = FF_GATEKEEPER_MODULE_ENABLED
|
||||
await deleteCheckoutSessionFactory({ db })({ checkoutSessionId: sessionId })
|
||||
return true
|
||||
},
|
||||
createCheckoutSession: async (parent, args, ctx) => {
|
||||
createCheckoutSession: async (_parent, args, ctx) => {
|
||||
let logger = extendLoggerComponent(
|
||||
ctx.log,
|
||||
'gatekeeper',
|
||||
@@ -160,25 +176,40 @@ export = FF_GATEKEEPER_MODULE_ENABLED
|
||||
Roles.Workspace.Admin,
|
||||
ctx.resourceAccessRules
|
||||
)
|
||||
|
||||
const createCheckoutSession = createCheckoutSessionFactory({
|
||||
stripe: getStripeClient(),
|
||||
frontendOrigin: getFrontendOrigin(),
|
||||
getWorkspacePlanPrice
|
||||
})
|
||||
|
||||
const createCheckoutSession = (await shouldUseNewCheckoutFlow(workspaceId))
|
||||
? createCheckoutSessionFactoryNew({
|
||||
stripe: getStripeClient(),
|
||||
frontendOrigin: getFrontendOrigin(),
|
||||
getWorkspacePlanPrice
|
||||
})
|
||||
: createCheckoutSessionFactoryOld({
|
||||
stripe: getStripeClient(),
|
||||
frontendOrigin: getFrontendOrigin(),
|
||||
getWorkspacePlanPrice
|
||||
})
|
||||
const countRole = countWorkspaceRoleWithOptionalProjectRoleFactory({ db })
|
||||
const startCheckoutSession = (await shouldUseNewCheckoutFlow(workspaceId))
|
||||
? startCheckoutSessionFactoryNew({
|
||||
getWorkspaceCheckoutSession: getWorkspaceCheckoutSessionFactory({ db }),
|
||||
getWorkspacePlan: getWorkspacePlanFactory({ db }),
|
||||
countSeatsByTypeInWorkspace: countSeatsByTypeInWorkspaceFactory({ db }),
|
||||
createCheckoutSession: createCheckoutSession as CreateCheckoutSession,
|
||||
saveCheckoutSession: saveCheckoutSessionFactory({ db }),
|
||||
deleteCheckoutSession: deleteCheckoutSessionFactory({ db })
|
||||
})
|
||||
: startCheckoutSessionFactoryOld({
|
||||
getWorkspaceCheckoutSession: getWorkspaceCheckoutSessionFactory({ db }),
|
||||
getWorkspacePlan: getWorkspacePlanFactory({ db }),
|
||||
countRole,
|
||||
createCheckoutSession:
|
||||
createCheckoutSession as CreateCheckoutSessionOld,
|
||||
saveCheckoutSession: saveCheckoutSessionFactory({ db }),
|
||||
deleteCheckoutSession: deleteCheckoutSessionFactory({ db })
|
||||
})
|
||||
|
||||
try {
|
||||
logger.info(OperationStatus.start, '[{operationName} ({operationStatus})]')
|
||||
const session = await startCheckoutSessionFactory({
|
||||
getWorkspaceCheckoutSession: getWorkspaceCheckoutSessionFactory({ db }),
|
||||
getWorkspacePlan: getWorkspacePlanFactory({ db }),
|
||||
countRole,
|
||||
createCheckoutSession,
|
||||
saveCheckoutSession: saveCheckoutSessionFactory({ db }),
|
||||
deleteCheckoutSession: deleteCheckoutSessionFactory({ db })
|
||||
})({
|
||||
const session = await startCheckoutSession({
|
||||
workspacePlan,
|
||||
workspaceId,
|
||||
workspaceSlug: workspace.slug,
|
||||
|
||||
@@ -1,131 +1,16 @@
|
||||
import {
|
||||
CheckoutSession,
|
||||
CreateCheckoutSession,
|
||||
GetCheckoutSession,
|
||||
GetWorkspacePlan,
|
||||
SaveCheckoutSession,
|
||||
UpdateCheckoutSessionStatus,
|
||||
UpsertWorkspaceSubscription,
|
||||
UpsertPaidWorkspacePlan,
|
||||
GetSubscriptionData,
|
||||
GetWorkspaceCheckoutSession,
|
||||
DeleteCheckoutSession
|
||||
GetSubscriptionData
|
||||
} from '@/modules/gatekeeper/domain/billing'
|
||||
import {
|
||||
CheckoutSessionNotFoundError,
|
||||
WorkspaceAlreadyPaidError,
|
||||
WorkspaceCheckoutSessionInProgressError
|
||||
WorkspaceAlreadyPaidError
|
||||
} from '@/modules/gatekeeper/errors/billing'
|
||||
import { throwUncoveredError } from '@speckle/shared'
|
||||
import { EventBusEmit } from '@/modules/shared/services/eventBus'
|
||||
import { CountWorkspaceRoleWithOptionalProjectRole } from '@/modules/workspaces/domain/operations'
|
||||
import {
|
||||
PaidWorkspacePlans,
|
||||
Roles,
|
||||
throwUncoveredError,
|
||||
WorkspacePlanBillingIntervals
|
||||
} from '@speckle/shared'
|
||||
|
||||
export const startCheckoutSessionFactory =
|
||||
({
|
||||
getWorkspaceCheckoutSession,
|
||||
deleteCheckoutSession,
|
||||
getWorkspacePlan,
|
||||
countRole,
|
||||
createCheckoutSession,
|
||||
saveCheckoutSession
|
||||
}: {
|
||||
getWorkspaceCheckoutSession: GetWorkspaceCheckoutSession
|
||||
deleteCheckoutSession: DeleteCheckoutSession
|
||||
getWorkspacePlan: GetWorkspacePlan
|
||||
countRole: CountWorkspaceRoleWithOptionalProjectRole
|
||||
createCheckoutSession: CreateCheckoutSession
|
||||
saveCheckoutSession: SaveCheckoutSession
|
||||
}) =>
|
||||
async ({
|
||||
workspaceId,
|
||||
workspaceSlug,
|
||||
workspacePlan,
|
||||
billingInterval,
|
||||
isCreateFlow
|
||||
}: {
|
||||
workspaceId: string
|
||||
workspaceSlug: string
|
||||
workspacePlan: PaidWorkspacePlans
|
||||
billingInterval: WorkspacePlanBillingIntervals
|
||||
isCreateFlow: boolean
|
||||
}): Promise<CheckoutSession> => {
|
||||
// get workspace plan, if we're already on a paid plan, do not allow checkout
|
||||
// paid plans should use a subscription modification
|
||||
const existingWorkspacePlan = await getWorkspacePlan({ workspaceId })
|
||||
|
||||
// it will technically not be possible to not have
|
||||
if (existingWorkspacePlan) {
|
||||
// maybe we can just ignore the plan not existing, cause we're putting it on a plan post checkout
|
||||
switch (existingWorkspacePlan.status) {
|
||||
// valid and paymentFailed, but not canceled status is not something we need a checkout for
|
||||
// we already have their credit card info
|
||||
case 'valid':
|
||||
case 'paymentFailed':
|
||||
case 'cancelationScheduled':
|
||||
throw new WorkspaceAlreadyPaidError()
|
||||
case 'canceled':
|
||||
const existingCheckoutSession = await getWorkspaceCheckoutSession({
|
||||
workspaceId
|
||||
})
|
||||
if (existingCheckoutSession)
|
||||
await deleteCheckoutSession({
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// if there is already a checkout session for the workspace, stop, someone else is maybe trying to pay for the workspace
|
||||
const workspaceCheckoutSession = await getWorkspaceCheckoutSession({
|
||||
workspaceId
|
||||
})
|
||||
if (workspaceCheckoutSession) {
|
||||
if (workspaceCheckoutSession.paymentStatus === 'paid')
|
||||
// this is should not be possible, but its better to be checking it here, than double charging the customer
|
||||
throw new WorkspaceAlreadyPaidError()
|
||||
if (new Date().getTime() - workspaceCheckoutSession.createdAt.getTime() > 1000) {
|
||||
await deleteCheckoutSession({
|
||||
checkoutSessionId: workspaceCheckoutSession.id
|
||||
})
|
||||
} else {
|
||||
throw new WorkspaceCheckoutSessionInProgressError()
|
||||
}
|
||||
}
|
||||
|
||||
const [adminCount, memberCount, guestCount] = await Promise.all([
|
||||
countRole({ workspaceId, workspaceRole: Roles.Workspace.Admin }),
|
||||
countRole({ workspaceId, workspaceRole: Roles.Workspace.Member }),
|
||||
countRole({ workspaceId, workspaceRole: Roles.Workspace.Guest })
|
||||
])
|
||||
|
||||
const checkoutSession = await createCheckoutSession({
|
||||
workspaceId,
|
||||
workspaceSlug,
|
||||
|
||||
billingInterval,
|
||||
workspacePlan,
|
||||
guestCount,
|
||||
seatCount: adminCount + memberCount,
|
||||
isCreateFlow
|
||||
})
|
||||
|
||||
await saveCheckoutSession({ checkoutSession })
|
||||
return checkoutSession
|
||||
}
|
||||
|
||||
export const completeCheckoutSessionFactory =
|
||||
({
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
import {
|
||||
CheckoutSession,
|
||||
CreateCheckoutSession,
|
||||
CreateCheckoutSessionOld,
|
||||
DeleteCheckoutSession,
|
||||
GetWorkspaceCheckoutSession,
|
||||
GetWorkspacePlan,
|
||||
SaveCheckoutSession
|
||||
} from '@/modules/gatekeeper/domain/billing'
|
||||
import { CountSeatsByTypeInWorkspace } from '@/modules/gatekeeper/domain/operations'
|
||||
import {
|
||||
InvalidWorkspacePlanUpgradeError,
|
||||
WorkspaceAlreadyPaidError,
|
||||
WorkspaceCheckoutSessionInProgressError
|
||||
} from '@/modules/gatekeeper/errors/billing'
|
||||
import { NotFoundError } from '@/modules/shared/errors'
|
||||
import { CountWorkspaceRoleWithOptionalProjectRole } from '@/modules/workspaces/domain/operations'
|
||||
import {
|
||||
PaidWorkspacePlans,
|
||||
Roles,
|
||||
throwUncoveredError,
|
||||
WorkspacePlanBillingIntervals,
|
||||
WorkspacePlans
|
||||
} from '@speckle/shared'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const startCheckoutSessionFactoryOld =
|
||||
({
|
||||
getWorkspaceCheckoutSession,
|
||||
deleteCheckoutSession,
|
||||
getWorkspacePlan,
|
||||
countRole,
|
||||
createCheckoutSession,
|
||||
saveCheckoutSession
|
||||
}: {
|
||||
getWorkspaceCheckoutSession: GetWorkspaceCheckoutSession
|
||||
deleteCheckoutSession: DeleteCheckoutSession
|
||||
getWorkspacePlan: GetWorkspacePlan
|
||||
countRole: CountWorkspaceRoleWithOptionalProjectRole
|
||||
createCheckoutSession: CreateCheckoutSessionOld
|
||||
saveCheckoutSession: SaveCheckoutSession
|
||||
}) =>
|
||||
async ({
|
||||
workspaceId,
|
||||
workspaceSlug,
|
||||
workspacePlan,
|
||||
billingInterval,
|
||||
isCreateFlow
|
||||
}: {
|
||||
workspaceId: string
|
||||
workspaceSlug: string
|
||||
workspacePlan: PaidWorkspacePlans
|
||||
billingInterval: WorkspacePlanBillingIntervals
|
||||
isCreateFlow: boolean
|
||||
}): Promise<CheckoutSession> => {
|
||||
// get workspace plan, if we're already on a paid plan, do not allow checkout
|
||||
// paid plans should use a subscription modification
|
||||
const existingWorkspacePlan = await getWorkspacePlan({ workspaceId })
|
||||
|
||||
if (existingWorkspacePlan) {
|
||||
// it will technically not be possible to not have
|
||||
// maybe we can just ignore the plan not existing, cause we're putting it on a plan post checkout
|
||||
switch (existingWorkspacePlan.status) {
|
||||
// valid and paymentFailed, but not canceled status is not something we need a checkout for
|
||||
// we already have their credit card info
|
||||
case 'valid':
|
||||
case 'paymentFailed':
|
||||
case 'cancelationScheduled':
|
||||
throw new WorkspaceAlreadyPaidError()
|
||||
case 'canceled':
|
||||
const existingCheckoutSession = await getWorkspaceCheckoutSession({
|
||||
workspaceId
|
||||
})
|
||||
if (existingCheckoutSession)
|
||||
await deleteCheckoutSession({
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// if there is already a checkout session for the workspace, stop, someone else is maybe trying to pay for the workspace
|
||||
const workspaceCheckoutSession = await getWorkspaceCheckoutSession({
|
||||
workspaceId
|
||||
})
|
||||
if (workspaceCheckoutSession) {
|
||||
if (workspaceCheckoutSession.paymentStatus === 'paid')
|
||||
// this is should not be possible, but its better to be checking it here, than double charging the customer
|
||||
throw new WorkspaceAlreadyPaidError()
|
||||
if (
|
||||
new Date().getTime() - workspaceCheckoutSession.createdAt.getTime() >
|
||||
1000
|
||||
// 10 * 60 * 1000
|
||||
) {
|
||||
await deleteCheckoutSession({
|
||||
checkoutSessionId: workspaceCheckoutSession.id
|
||||
})
|
||||
} else {
|
||||
throw new WorkspaceCheckoutSessionInProgressError()
|
||||
}
|
||||
}
|
||||
|
||||
const [adminCount, memberCount, guestCount] = await Promise.all([
|
||||
countRole({ workspaceId, workspaceRole: Roles.Workspace.Admin }),
|
||||
countRole({ workspaceId, workspaceRole: Roles.Workspace.Member }),
|
||||
countRole({ workspaceId, workspaceRole: Roles.Workspace.Guest })
|
||||
])
|
||||
|
||||
const checkoutSession = await createCheckoutSession({
|
||||
workspaceId,
|
||||
workspaceSlug,
|
||||
billingInterval,
|
||||
workspacePlan,
|
||||
guestCount,
|
||||
seatCount: adminCount + memberCount,
|
||||
isCreateFlow
|
||||
})
|
||||
|
||||
await saveCheckoutSession({ checkoutSession })
|
||||
return checkoutSession
|
||||
}
|
||||
|
||||
const WorkspacePlansUpgradeMapping = z.union([
|
||||
z.object({
|
||||
current: z.literal('free'),
|
||||
upgrade: z.union([z.literal('team'), z.literal('pro')])
|
||||
}),
|
||||
z.object({
|
||||
current: z.literal('team'),
|
||||
upgrade: z.literal('pro')
|
||||
})
|
||||
])
|
||||
|
||||
const isUpgradeWorkspacePlanValid = ({
|
||||
current,
|
||||
upgrade
|
||||
}: {
|
||||
current: WorkspacePlans
|
||||
upgrade: WorkspacePlans
|
||||
}): boolean => {
|
||||
return WorkspacePlansUpgradeMapping.safeParse({ current, upgrade }).success
|
||||
}
|
||||
|
||||
export const startCheckoutSessionFactoryNew =
|
||||
({
|
||||
getWorkspaceCheckoutSession,
|
||||
deleteCheckoutSession,
|
||||
getWorkspacePlan,
|
||||
countSeatsByTypeInWorkspace,
|
||||
createCheckoutSession,
|
||||
saveCheckoutSession
|
||||
}: {
|
||||
getWorkspaceCheckoutSession: GetWorkspaceCheckoutSession
|
||||
deleteCheckoutSession: DeleteCheckoutSession
|
||||
getWorkspacePlan: GetWorkspacePlan
|
||||
countSeatsByTypeInWorkspace: CountSeatsByTypeInWorkspace
|
||||
createCheckoutSession: CreateCheckoutSession
|
||||
saveCheckoutSession: SaveCheckoutSession
|
||||
}) =>
|
||||
async ({
|
||||
workspaceId,
|
||||
workspaceSlug,
|
||||
workspacePlan,
|
||||
billingInterval,
|
||||
isCreateFlow
|
||||
}: {
|
||||
workspaceId: string
|
||||
workspaceSlug: string
|
||||
workspacePlan: PaidWorkspacePlans
|
||||
billingInterval: WorkspacePlanBillingIntervals
|
||||
isCreateFlow: boolean
|
||||
}): Promise<CheckoutSession> => {
|
||||
const existingWorkspacePlan = await getWorkspacePlan({ workspaceId })
|
||||
|
||||
if (!existingWorkspacePlan) {
|
||||
// New plans are enabled so we assume a plan is always present (the free plan)
|
||||
throw new NotFoundError('Workspace does not have a plan', {
|
||||
info: { workspaceId }
|
||||
})
|
||||
}
|
||||
|
||||
const upgradeValid = isUpgradeWorkspacePlanValid({
|
||||
current: existingWorkspacePlan.name,
|
||||
upgrade: workspacePlan
|
||||
})
|
||||
if (!upgradeValid) {
|
||||
throw new InvalidWorkspacePlanUpgradeError(null, {
|
||||
info: {
|
||||
workspaceId,
|
||||
currentPlan: existingWorkspacePlan.name,
|
||||
upgradePlan: workspacePlan
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// it will technically not be possible to not have
|
||||
// maybe we can just ignore the plan not existing, cause we're putting it on a plan post checkout
|
||||
switch (existingWorkspacePlan.status) {
|
||||
// valid and paymentFailed, but not canceled status is not something we need a checkout for
|
||||
// we already have their credit card info
|
||||
case 'valid':
|
||||
case 'paymentFailed':
|
||||
case 'cancelationScheduled':
|
||||
if (existingWorkspacePlan.name === 'free') {
|
||||
break
|
||||
}
|
||||
throw new WorkspaceAlreadyPaidError()
|
||||
case 'canceled':
|
||||
const existingCheckoutSession = await getWorkspaceCheckoutSession({
|
||||
workspaceId
|
||||
})
|
||||
if (existingCheckoutSession)
|
||||
await deleteCheckoutSession({
|
||||
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)
|
||||
}
|
||||
|
||||
// if there is already a checkout session for the workspace, stop, someone else is maybe trying to pay for the workspace
|
||||
const workspaceCheckoutSession = await getWorkspaceCheckoutSession({
|
||||
workspaceId
|
||||
})
|
||||
if (workspaceCheckoutSession) {
|
||||
if (workspaceCheckoutSession.paymentStatus === 'paid')
|
||||
// this is should not be possible, but its better to be checking it here, than double charging the customer
|
||||
throw new WorkspaceAlreadyPaidError()
|
||||
if (
|
||||
new Date().getTime() - workspaceCheckoutSession.createdAt.getTime() >
|
||||
1000
|
||||
// 10 * 60 * 1000
|
||||
) {
|
||||
await deleteCheckoutSession({
|
||||
checkoutSessionId: workspaceCheckoutSession.id
|
||||
})
|
||||
} else {
|
||||
throw new WorkspaceCheckoutSessionInProgressError()
|
||||
}
|
||||
}
|
||||
|
||||
const [editorsCount, viewersCount] = await Promise.all([
|
||||
countSeatsByTypeInWorkspace({ workspaceId, type: 'editor' }),
|
||||
countSeatsByTypeInWorkspace({ workspaceId, type: 'viewer' })
|
||||
])
|
||||
|
||||
const checkoutSession = await createCheckoutSession({
|
||||
workspaceId,
|
||||
workspaceSlug,
|
||||
billingInterval,
|
||||
workspacePlan,
|
||||
viewersCount,
|
||||
editorsCount,
|
||||
isCreateFlow
|
||||
})
|
||||
|
||||
await saveCheckoutSession({ checkoutSession })
|
||||
return checkoutSession
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export const getStripeClient = () => {
|
||||
return stripeClient
|
||||
}
|
||||
|
||||
const { FF_WORKSPACES_NEW_PLAN_ENABLED } = getFeatureFlags()
|
||||
const { FF_WORKSPACES_NEW_PLANS_ENABLED } = getFeatureFlags()
|
||||
|
||||
export const workspacePlanPrices = (): Record<
|
||||
WorkspacePricingProducts,
|
||||
@@ -46,8 +46,13 @@ export const workspacePlanPrices = (): Record<
|
||||
yearly: getStringFromEnv('WORKSPACE_YEARLY_BUSINESS_SEAT_STRIPE_PRICE_ID')
|
||||
},
|
||||
// new
|
||||
...((FF_WORKSPACES_NEW_PLAN_ENABLED
|
||||
...((FF_WORKSPACES_NEW_PLANS_ENABLED
|
||||
? {
|
||||
viewer: {
|
||||
productId: getStringFromEnv('WORKSPACE_VIEWER_SEAT_STRIPE_PRODUCT_ID'),
|
||||
monthly: getStringFromEnv('WORKSPACE_MONTHLY_VIEWER_SEAT_STRIPE_PRICE_ID'),
|
||||
yearly: getStringFromEnv('WORKSPACE_YEARLY_VIEWER_SEAT_STRIPE_PRICE_ID')
|
||||
},
|
||||
team: {
|
||||
productId: getStringFromEnv('WORKSPACE_TEAM_SEAT_STRIPE_PRODUCT_ID'),
|
||||
monthly: getStringFromEnv('WORKSPACE_MONTHLY_TEAM_SEAT_STRIPE_PRICE_ID'),
|
||||
@@ -60,7 +65,7 @@ export const workspacePlanPrices = (): Record<
|
||||
}
|
||||
}
|
||||
: {}) as Record<
|
||||
'team' | 'pro',
|
||||
'viewer' | 'team' | 'pro',
|
||||
Record<WorkspacePlanBillingIntervals, string> & { productId: string }
|
||||
>)
|
||||
})
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import {
|
||||
CheckoutSessionNotFoundError,
|
||||
InvalidWorkspacePlanUpgradeError,
|
||||
WorkspaceAlreadyPaidError,
|
||||
WorkspaceCheckoutSessionInProgressError
|
||||
} from '@/modules/gatekeeper/errors/billing'
|
||||
import {
|
||||
completeCheckoutSessionFactory,
|
||||
startCheckoutSessionFactory
|
||||
} from '@/modules/gatekeeper/services/checkout'
|
||||
import { completeCheckoutSessionFactory } from '@/modules/gatekeeper/services/checkout'
|
||||
import { expectToThrow } from '@/test/assertionHelper'
|
||||
import { expect } from 'chai'
|
||||
import cryptoRandomString from 'crypto-random-string'
|
||||
@@ -18,13 +16,18 @@ import {
|
||||
import { omit } from 'lodash'
|
||||
import { PaidWorkspacePlan } from '@/modules/gatekeeperCore/domain/billing'
|
||||
import { PaidWorkspacePlans, WorkspacePlanBillingIntervals } from '@speckle/shared'
|
||||
import {
|
||||
startCheckoutSessionFactoryNew as startCheckoutSessionFactory,
|
||||
startCheckoutSessionFactoryOld
|
||||
} from '@/modules/gatekeeper/services/checkout/startCheckoutSession'
|
||||
import { NotFoundError } from '@/modules/shared/errors'
|
||||
|
||||
describe('checkout @gatekeeper', () => {
|
||||
describe('startCheckoutSessionFactory creates a function, that', () => {
|
||||
describe('startCheckoutSessionFactoryOld creates a function, that', () => {
|
||||
it('does not allow checkout for workspace plans, that is in a valid state', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const err = await expectToThrow(() =>
|
||||
startCheckoutSessionFactory({
|
||||
startCheckoutSessionFactoryOld({
|
||||
getWorkspacePlan: async () => ({
|
||||
name: 'plus',
|
||||
status: 'valid',
|
||||
@@ -59,7 +62,7 @@ describe('checkout @gatekeeper', () => {
|
||||
it('does not allow checkout for workspace plans, that is in a paymentFailed state', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const err = await expectToThrow(() =>
|
||||
startCheckoutSessionFactory({
|
||||
startCheckoutSessionFactoryOld({
|
||||
getWorkspacePlan: async () => ({
|
||||
name: 'plus',
|
||||
status: 'paymentFailed',
|
||||
@@ -94,7 +97,7 @@ describe('checkout @gatekeeper', () => {
|
||||
it('does not allow checkout for a workspace, that already has a recent checkout session', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const err = await expectToThrow(() =>
|
||||
startCheckoutSessionFactory({
|
||||
startCheckoutSessionFactoryOld({
|
||||
getWorkspacePlan: async () => ({
|
||||
name: 'starter',
|
||||
status: 'trial',
|
||||
@@ -139,7 +142,7 @@ describe('checkout @gatekeeper', () => {
|
||||
it('does not allow checkout for a workspace, that already has a checkout session', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const err = await expectToThrow(() =>
|
||||
startCheckoutSessionFactory({
|
||||
startCheckoutSessionFactoryOld({
|
||||
getWorkspacePlan: async () => ({
|
||||
name: 'starter',
|
||||
status: 'trial',
|
||||
@@ -197,7 +200,7 @@ describe('checkout @gatekeeper', () => {
|
||||
updatedAt: new Date()
|
||||
}
|
||||
let storedCheckoutSession: CheckoutSession | undefined = undefined
|
||||
const createdCheckoutSession = await startCheckoutSessionFactory({
|
||||
const createdCheckoutSession = await startCheckoutSessionFactoryOld({
|
||||
getWorkspacePlan: async () => null,
|
||||
getWorkspaceCheckoutSession: async () => null,
|
||||
countRole: async () => 1,
|
||||
@@ -233,7 +236,7 @@ describe('checkout @gatekeeper', () => {
|
||||
updatedAt: new Date()
|
||||
}
|
||||
let storedCheckoutSession: CheckoutSession | undefined = undefined
|
||||
const createdCheckoutSession = await startCheckoutSessionFactory({
|
||||
const createdCheckoutSession = await startCheckoutSessionFactoryOld({
|
||||
getWorkspacePlan: async () => null,
|
||||
getWorkspaceCheckoutSession: async () => null,
|
||||
countRole: async () => 1,
|
||||
@@ -270,7 +273,7 @@ describe('checkout @gatekeeper', () => {
|
||||
updatedAt: new Date()
|
||||
}
|
||||
let storedCheckoutSession: CheckoutSession | undefined = undefined
|
||||
const createdCheckoutSession = await startCheckoutSessionFactory({
|
||||
const createdCheckoutSession = await startCheckoutSessionFactoryOld({
|
||||
getWorkspacePlan: async () => ({
|
||||
workspaceId,
|
||||
name: 'starter',
|
||||
@@ -322,7 +325,7 @@ describe('checkout @gatekeeper', () => {
|
||||
workspacePlan
|
||||
}
|
||||
let storedCheckoutSession: CheckoutSession | undefined = undefined
|
||||
const createdCheckoutSession = await startCheckoutSessionFactory({
|
||||
const createdCheckoutSession = await startCheckoutSessionFactoryOld({
|
||||
getWorkspacePlan: async () => ({
|
||||
workspaceId,
|
||||
name: 'starter',
|
||||
@@ -365,7 +368,7 @@ describe('checkout @gatekeeper', () => {
|
||||
workspacePlan
|
||||
}
|
||||
const err = await expectToThrow(async () => {
|
||||
await startCheckoutSessionFactory({
|
||||
await startCheckoutSessionFactoryOld({
|
||||
getWorkspacePlan: async () => ({
|
||||
workspaceId,
|
||||
name: 'starter',
|
||||
@@ -407,7 +410,7 @@ describe('checkout @gatekeeper', () => {
|
||||
workspacePlan
|
||||
}
|
||||
const err = await expectToThrow(async () => {
|
||||
await startCheckoutSessionFactory({
|
||||
await startCheckoutSessionFactoryOld({
|
||||
getWorkspacePlan: async () => ({
|
||||
workspaceId,
|
||||
name: 'starter',
|
||||
@@ -461,7 +464,7 @@ describe('checkout @gatekeeper', () => {
|
||||
updatedAt: new Date()
|
||||
}
|
||||
let storedCheckoutSession: CheckoutSession | undefined = undefined
|
||||
const createdCheckoutSession = await startCheckoutSessionFactory({
|
||||
const createdCheckoutSession = await startCheckoutSessionFactoryOld({
|
||||
getWorkspacePlan: async () => ({
|
||||
name: 'plus',
|
||||
workspaceId,
|
||||
@@ -489,6 +492,7 @@ describe('checkout @gatekeeper', () => {
|
||||
expect(checkoutSession).deep.equal(createdCheckoutSession)
|
||||
})
|
||||
})
|
||||
|
||||
describe('completeCheckoutSessionFactory creates a function, that', () => {
|
||||
it('throws a CheckoutSessionNotFound if the checkoutSession is null', async () => {
|
||||
const sessionId = cryptoRandomString({ length: 10 })
|
||||
@@ -651,4 +655,378 @@ describe('checkout @gatekeeper', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('startCheckoutSessionFactory creates a function, that', () => {
|
||||
it('does not allow checkout if workspace plan does not exists', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const err = await expectToThrow(() =>
|
||||
startCheckoutSessionFactory({
|
||||
getWorkspacePlan: async () => null,
|
||||
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
|
||||
})
|
||||
)
|
||||
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
|
||||
})
|
||||
)
|
||||
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(() =>
|
||||
startCheckoutSessionFactory({
|
||||
getWorkspacePlan: async () => ({
|
||||
name: 'team',
|
||||
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
|
||||
})
|
||||
)
|
||||
expect(err.name).to.be.equal(new WorkspaceAlreadyPaidError().name)
|
||||
})
|
||||
it('does not allow checkout for workspace plans, that is in a paymentFailed state', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const err = await expectToThrow(() =>
|
||||
startCheckoutSessionFactory({
|
||||
getWorkspacePlan: async () => ({
|
||||
name: 'team',
|
||||
status: 'paymentFailed',
|
||||
createdAt: new Date(),
|
||||
workspaceId
|
||||
}),
|
||||
getWorkspaceCheckoutSession: () => {
|
||||
expect.fail()
|
||||
},
|
||||
countSeatsByTypeInWorkspace: () => {
|
||||
expect.fail()
|
||||
},
|
||||
createCheckoutSession: () => {
|
||||
expect.fail()
|
||||
},
|
||||
deleteCheckoutSession: () => {
|
||||
expect.fail()
|
||||
},
|
||||
saveCheckoutSession: () => {
|
||||
expect.fail()
|
||||
}
|
||||
})({
|
||||
workspaceId,
|
||||
billingInterval: 'monthly',
|
||||
workspacePlan: 'pro',
|
||||
workspaceSlug: cryptoRandomString({ length: 10 }),
|
||||
isCreateFlow: false
|
||||
})
|
||||
)
|
||||
expect(err.message).to.be.equal(new WorkspaceAlreadyPaidError().message)
|
||||
})
|
||||
it('does not allow checkout for a workspace, that already has a checkout session', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const err = await expectToThrow(() =>
|
||||
startCheckoutSessionFactory({
|
||||
getWorkspacePlan: async () => ({
|
||||
name: 'free',
|
||||
status: 'valid',
|
||||
createdAt: new Date(),
|
||||
|
||||
workspaceId
|
||||
}),
|
||||
getWorkspaceCheckoutSession: async () => ({
|
||||
billingInterval: 'monthly',
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
paymentStatus: 'unpaid',
|
||||
url: '',
|
||||
workspaceId,
|
||||
workspacePlan: 'business',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}),
|
||||
countSeatsByTypeInWorkspace: () => {
|
||||
expect.fail()
|
||||
},
|
||||
createCheckoutSession: () => {
|
||||
expect.fail()
|
||||
},
|
||||
|
||||
deleteCheckoutSession: () => {
|
||||
expect.fail()
|
||||
},
|
||||
saveCheckoutSession: () => {
|
||||
expect.fail()
|
||||
}
|
||||
})({
|
||||
workspaceId,
|
||||
billingInterval: 'monthly',
|
||||
workspacePlan: 'team',
|
||||
workspaceSlug: cryptoRandomString({ length: 10 }),
|
||||
isCreateFlow: false
|
||||
})
|
||||
)
|
||||
expect(err.message).to.be.equal(
|
||||
new WorkspaceCheckoutSessionInProgressError().message
|
||||
)
|
||||
})
|
||||
|
||||
it('creates and stores a checkout for FREE workspaces', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const workspacePlan: PaidWorkspacePlans = 'pro'
|
||||
const billingInterval: WorkspacePlanBillingIntervals = 'monthly'
|
||||
const checkoutSession: CheckoutSession = {
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
workspaceId,
|
||||
workspacePlan,
|
||||
url: 'https://example.com',
|
||||
billingInterval,
|
||||
paymentStatus: 'unpaid',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
let storedCheckoutSession: CheckoutSession | undefined = undefined
|
||||
const createdCheckoutSession = await startCheckoutSessionFactory({
|
||||
getWorkspacePlan: async () => ({
|
||||
workspaceId,
|
||||
name: 'free',
|
||||
createdAt: new Date(),
|
||||
status: 'valid'
|
||||
}),
|
||||
getWorkspaceCheckoutSession: async () => null,
|
||||
countSeatsByTypeInWorkspace: async () => 1,
|
||||
deleteCheckoutSession: () => {
|
||||
expect.fail()
|
||||
},
|
||||
createCheckoutSession: async () => checkoutSession,
|
||||
saveCheckoutSession: async ({ checkoutSession }) => {
|
||||
storedCheckoutSession = checkoutSession
|
||||
}
|
||||
})({
|
||||
workspaceId,
|
||||
billingInterval,
|
||||
workspacePlan,
|
||||
workspaceSlug: cryptoRandomString({ length: 10 }),
|
||||
isCreateFlow: false
|
||||
})
|
||||
expect(checkoutSession).deep.equal(storedCheckoutSession)
|
||||
expect(checkoutSession).deep.equal(createdCheckoutSession)
|
||||
})
|
||||
|
||||
it('creates and stores a checkout for FREE workspaces even if it has an old unpaid checkout session', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const workspacePlan: PaidWorkspacePlans = 'team'
|
||||
const billingInterval: WorkspacePlanBillingIntervals = 'monthly'
|
||||
const checkoutSession: CheckoutSession = {
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
workspaceId,
|
||||
workspacePlan,
|
||||
url: 'https://example.com',
|
||||
billingInterval,
|
||||
paymentStatus: 'unpaid',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
let existingCheckoutSession: CheckoutSession | undefined = {
|
||||
billingInterval,
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
createdAt: new Date(1990, 1, 12),
|
||||
updatedAt: new Date(1990, 1, 12),
|
||||
paymentStatus: 'unpaid',
|
||||
url: 'https://example.com',
|
||||
workspaceId,
|
||||
workspacePlan
|
||||
}
|
||||
let storedCheckoutSession: CheckoutSession | undefined = undefined
|
||||
const createdCheckoutSession = await startCheckoutSessionFactory({
|
||||
getWorkspacePlan: async () => ({
|
||||
workspaceId,
|
||||
name: 'free',
|
||||
status: 'valid',
|
||||
createdAt: new Date()
|
||||
}),
|
||||
getWorkspaceCheckoutSession: async () => existingCheckoutSession!,
|
||||
countSeatsByTypeInWorkspace: async () => 1,
|
||||
deleteCheckoutSession: async () => {
|
||||
existingCheckoutSession = undefined
|
||||
},
|
||||
createCheckoutSession: async () => checkoutSession,
|
||||
saveCheckoutSession: async ({ checkoutSession }) => {
|
||||
storedCheckoutSession = checkoutSession
|
||||
}
|
||||
})({
|
||||
workspaceId,
|
||||
billingInterval,
|
||||
workspacePlan,
|
||||
workspaceSlug: cryptoRandomString({ length: 10 }),
|
||||
isCreateFlow: false
|
||||
})
|
||||
expect(existingCheckoutSession).to.be.undefined
|
||||
expect(checkoutSession).deep.equal(storedCheckoutSession)
|
||||
expect(checkoutSession).deep.equal(createdCheckoutSession)
|
||||
})
|
||||
|
||||
it('does not allow checkout for FREE workspaces if there is a paid checkout session', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const workspacePlan: PaidWorkspacePlans = 'pro'
|
||||
const billingInterval: WorkspacePlanBillingIntervals = 'monthly'
|
||||
let existingCheckoutSession: CheckoutSession | undefined = {
|
||||
billingInterval,
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
createdAt: new Date(1990, 1, 12),
|
||||
updatedAt: new Date(1990, 1, 12),
|
||||
paymentStatus: 'paid',
|
||||
url: 'https://example.com',
|
||||
workspaceId,
|
||||
workspacePlan
|
||||
}
|
||||
const err = await expectToThrow(async () => {
|
||||
await startCheckoutSessionFactory({
|
||||
getWorkspacePlan: async () => ({
|
||||
workspaceId,
|
||||
name: 'free',
|
||||
createdAt: new Date(),
|
||||
status: 'valid'
|
||||
}),
|
||||
getWorkspaceCheckoutSession: async () => existingCheckoutSession!,
|
||||
countSeatsByTypeInWorkspace: async () => 1,
|
||||
deleteCheckoutSession: async () => {
|
||||
existingCheckoutSession = undefined
|
||||
},
|
||||
createCheckoutSession: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
saveCheckoutSession: async () => {}
|
||||
})({
|
||||
workspaceId,
|
||||
billingInterval,
|
||||
workspacePlan,
|
||||
workspaceSlug: cryptoRandomString({ length: 10 }),
|
||||
isCreateFlow: false
|
||||
})
|
||||
})
|
||||
expect(err.message).to.equal(new WorkspaceAlreadyPaidError().message)
|
||||
})
|
||||
|
||||
it('creates and stores a checkout for CANCELED workspaces', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const workspacePlan: PaidWorkspacePlans = 'pro'
|
||||
const billingInterval: WorkspacePlanBillingIntervals = 'monthly'
|
||||
const checkoutSession: CheckoutSession = {
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
workspaceId,
|
||||
workspacePlan,
|
||||
url: 'https://example.com',
|
||||
billingInterval,
|
||||
paymentStatus: 'unpaid',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
let existingCheckoutSession: CheckoutSession | undefined = {
|
||||
billingInterval: 'monthly',
|
||||
id: cryptoRandomString({ length: 10 }),
|
||||
paymentStatus: 'paid',
|
||||
url: '',
|
||||
workspaceId,
|
||||
workspacePlan: 'team',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}
|
||||
let storedCheckoutSession: CheckoutSession | undefined = undefined
|
||||
const createdCheckoutSession = await startCheckoutSessionFactory({
|
||||
getWorkspacePlan: async () => ({
|
||||
name: 'team',
|
||||
workspaceId,
|
||||
createdAt: new Date(),
|
||||
status: 'canceled'
|
||||
}),
|
||||
getWorkspaceCheckoutSession: async () => existingCheckoutSession!,
|
||||
countSeatsByTypeInWorkspace: async () => 1,
|
||||
deleteCheckoutSession: async () => {
|
||||
existingCheckoutSession = undefined
|
||||
},
|
||||
createCheckoutSession: async () => checkoutSession,
|
||||
saveCheckoutSession: async ({ checkoutSession }) => {
|
||||
storedCheckoutSession = checkoutSession
|
||||
}
|
||||
})({
|
||||
workspaceId,
|
||||
billingInterval,
|
||||
workspacePlan,
|
||||
workspaceSlug: cryptoRandomString({ length: 10 }),
|
||||
isCreateFlow: false
|
||||
})
|
||||
expect(existingCheckoutSession).to.be.undefined
|
||||
expect(checkoutSession).deep.equal(storedCheckoutSession)
|
||||
expect(checkoutSession).deep.equal(createdCheckoutSession)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
/**
|
||||
* This includes the pricing plans (Stripe products) a customer can sub to
|
||||
*/
|
||||
export type WorkspacePricingProducts = PaidWorkspacePlans | 'guest'
|
||||
export type WorkspacePricingProducts = PaidWorkspacePlans | 'guest' | 'viewer'
|
||||
|
||||
type BaseWorkspacePlan = {
|
||||
workspaceId: string
|
||||
|
||||
@@ -1876,7 +1876,9 @@ export type OnboardingCompletionInput = {
|
||||
export const PaidWorkspacePlans = {
|
||||
Business: 'business',
|
||||
Plus: 'plus',
|
||||
Starter: 'starter'
|
||||
Pro: 'pro',
|
||||
Starter: 'starter',
|
||||
Team: 'team'
|
||||
} as const;
|
||||
|
||||
export type PaidWorkspacePlans = typeof PaidWorkspacePlans[keyof typeof PaidWorkspacePlans];
|
||||
|
||||
@@ -80,10 +80,6 @@ const parseFeatureFlags = () => {
|
||||
FF_MOVE_PROJECT_REGION_ENABLED: {
|
||||
schema: z.boolean(),
|
||||
defaults: { production: false, _: true }
|
||||
},
|
||||
FF_WORKSPACES_NEW_PLAN_ENABLED: {
|
||||
schema: z.boolean(),
|
||||
defaults: { production: false, _: false }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -113,7 +109,6 @@ export function getFeatureFlags(): {
|
||||
FF_FORCE_ONBOARDING: boolean
|
||||
FF_OBJECTS_STREAMING_FIX: boolean
|
||||
FF_MOVE_PROJECT_REGION_ENABLED: boolean
|
||||
FF_WORKSPACES_NEW_PLAN_ENABLED: boolean
|
||||
FF_NO_PERSONAL_EMAILS_ENABLED: boolean
|
||||
} {
|
||||
if (!parsedFlags) parsedFlags = parseFeatureFlags()
|
||||
|
||||
Reference in New Issue
Block a user