Files
speckle-server/packages/server/modules/gatekeeper/services/checkout/startCheckoutSession.ts
T
Daniel Gak Anagrov 51d6a8dd67 feat(activity): added user info to checkout_subscription and subscription upgrade (#4967)
* feat: added userId to checkout_subscription
* feat: add update intent to subscription
2025-06-24 15:34:26 +02:00

149 lines
4.5 KiB
TypeScript

import {
CheckoutSession,
CreateCheckoutSession,
Currency,
DeleteCheckoutSession,
GetWorkspaceCheckoutSession,
GetWorkspacePlan,
SaveCheckoutSession,
WorkspaceSeatType
} from '@/modules/gatekeeper/domain/billing'
import { CountSeatsByTypeInWorkspace } from '@/modules/gatekeeper/domain/operations'
import {
InvalidWorkspacePlanUpgradeError,
WorkspaceAlreadyPaidError,
WorkspaceCheckoutSessionInProgressError
} from '@/modules/gatekeeper/errors/billing'
import { isUpgradeWorkspacePlanValid } from '@/modules/gatekeeper/services/upgrades'
import { NotFoundError } from '@/modules/shared/errors'
import {
PaidWorkspacePlans,
throwUncoveredError,
TIME_MS,
WorkspacePlanBillingIntervals
} from '@speckle/shared'
export const startCheckoutSessionFactory =
({
getWorkspaceCheckoutSession,
deleteCheckoutSession,
getWorkspacePlan,
countSeatsByTypeInWorkspace,
createCheckoutSession,
saveCheckoutSession
}: {
getWorkspaceCheckoutSession: GetWorkspaceCheckoutSession
deleteCheckoutSession: DeleteCheckoutSession
getWorkspacePlan: GetWorkspacePlan
countSeatsByTypeInWorkspace: CountSeatsByTypeInWorkspace
createCheckoutSession: CreateCheckoutSession
saveCheckoutSession: SaveCheckoutSession
}) =>
async ({
workspaceId,
userId,
workspaceSlug,
workspacePlan,
billingInterval,
isCreateFlow,
currency
}: {
workspaceId: string
userId: string
workspaceSlug: string
workspacePlan: PaidWorkspacePlans
billingInterval: WorkspacePlanBillingIntervals
isCreateFlow: boolean
currency: Currency
}): 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
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() >
1 * TIME_MS.second
) {
await deleteCheckoutSession({
checkoutSessionId: workspaceCheckoutSession.id
})
} else {
throw new WorkspaceCheckoutSessionInProgressError()
}
}
const editorsCount = await countSeatsByTypeInWorkspace({
workspaceId,
type: WorkspaceSeatType.Editor
})
if (!editorsCount) {
throw new InvalidWorkspacePlanUpgradeError('Workspace has no seats')
}
const checkoutSession = await createCheckoutSession({
workspaceId,
userId,
workspaceSlug,
billingInterval,
workspacePlan,
editorsCount,
isCreateFlow,
currency
})
await saveCheckoutSession({ checkoutSession })
return checkoutSession
}