Files
speckle-server/packages/server/modules/gatekeeper/domain/billing.ts
T
Gergő Jedlicska f501cc4ad5 gergo/web 2888 workspace project cancreate (#4294)
* WIP can create project

* WIP can create project more work

* complete body, stencil tests

* feat(shared): move workspace plan types into shared

* test progress wip

* feat(shared): add more logic to canCreateWorkspaceProject

* a few more tests, as a treat

* chore(authz): round out tests

* fixed loaders, new GQL checks, dataLoaders in auth loaders

* fix(authz): get workspace limits loader

* chore(authz): update loaders

* frontend fixed up to snuff

* fix(authz): fix workspace plans for tests

* fix(authz): classic

* fix(authz): 0 counts

---------

Co-authored-by: Chuck Driesler <chuck@speckle.systems>
Co-authored-by: Kristaps Fabians Geikins <fabis94@live.com>
2025-04-01 16:38:20 +01:00

250 lines
6.4 KiB
TypeScript

import {
WorkspacePlanProductPrices,
WorkspacePricingProducts
} from '@/modules/gatekeeperCore/domain/billing'
import { Workspace, WorkspaceAcl } from '@/modules/workspacesCore/domain/types'
import {
Nullable,
Optional,
PaidWorkspacePlan,
PaidWorkspacePlans,
TrialWorkspacePlan,
UnpaidWorkspacePlan,
WorkspacePlan,
WorkspacePlanBillingIntervals
} from '@speckle/shared'
import { OverrideProperties } from 'type-fest'
import { z } from 'zod'
export type GetWorkspacePlan = (args: {
workspaceId: string
}) => Promise<WorkspacePlan | null>
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>
export type UpsertUnpaidWorkspacePlan = (args: {
workspacePlan: UnpaidWorkspacePlan
}) => Promise<void>
export type UpsertWorkspacePlan = (args: {
workspacePlan: WorkspacePlan
}) => Promise<void>
export type SessionInput = {
id: string
}
export type SessionPaymentStatus = 'paid' | 'unpaid'
export type CheckoutSession = SessionInput & {
url: string
workspaceId: string
workspacePlan: PaidWorkspacePlans
paymentStatus: SessionPaymentStatus
billingInterval: WorkspacePlanBillingIntervals
createdAt: Date
updatedAt: Date
}
export type SaveCheckoutSession = (args: {
checkoutSession: CheckoutSession
}) => Promise<void>
export type GetCheckoutSession = (args: {
sessionId: string
}) => Promise<CheckoutSession | null>
export type DeleteCheckoutSession = (args: {
checkoutSessionId: string
}) => Promise<void>
export type GetWorkspaceCheckoutSession = (args: {
workspaceId: string
}) => Promise<CheckoutSession | null>
export type UpdateCheckoutSessionStatus = (args: {
sessionId: string
paymentStatus: SessionPaymentStatus
}) => Promise<void>
// Remove with FF_WORKSPACES_NEW_PLANS_ENABLED
export type CreateCheckoutSessionOld = (args: {
workspaceId: string
workspaceSlug: string
seatCount: number
guestCount: number
workspacePlan: PaidWorkspacePlans
billingInterval: WorkspacePlanBillingIntervals
isCreateFlow: boolean
}) => Promise<CheckoutSession>
export type CreateCheckoutSession = (args: {
workspaceId: string
workspaceSlug: string
editorsCount: number
workspacePlan: PaidWorkspacePlans
billingInterval: WorkspacePlanBillingIntervals
isCreateFlow: boolean
}) => Promise<CheckoutSession>
export type WorkspaceSubscription = {
workspaceId: string
createdAt: Date
updatedAt: Date
currentBillingCycleEnd: Date
billingInterval: WorkspacePlanBillingIntervals
subscriptionData: SubscriptionData
}
const subscriptionProduct = z.object({
productId: z.string(),
subscriptionItemId: z.string(),
priceId: z.string(),
quantity: z.number()
})
export type SubscriptionProduct = z.infer<typeof subscriptionProduct>
export const SubscriptionData = z.object({
subscriptionId: z.string().min(1),
customerId: z.string().min(1),
cancelAt: z.date().nullable(),
status: z.union([
z.literal('incomplete'),
z.literal('incomplete_expired'),
z.literal('trialing'),
z.literal('active'),
z.literal('past_due'),
z.literal('canceled'),
z.literal('unpaid'),
z.literal('paused')
]),
products: subscriptionProduct.array(),
currentPeriodEnd: z.coerce.date()
})
// this abstracts the stripe sub data
export type SubscriptionData = z.infer<typeof SubscriptionData>
export const calculateSubscriptionSeats = ({
subscriptionData,
guestSeatProductId
}: {
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 }
}
export type UpsertWorkspaceSubscription = (args: {
workspaceSubscription: WorkspaceSubscription
}) => Promise<void>
export type GetWorkspaceSubscription = (args: {
workspaceId: string
}) => Promise<WorkspaceSubscription | null>
export type GetWorkspaceSubscriptions = () => Promise<WorkspaceSubscription[]>
export type GetWorkspaceSubscriptionBySubscriptionId = (args: {
subscriptionId: string
}) => Promise<WorkspaceSubscription | null>
export type GetSubscriptionData = (args: {
subscriptionId: string
}) => Promise<SubscriptionData>
export type GetWorkspacePlanPriceId = (args: {
workspacePlan: WorkspacePricingProducts
billingInterval: WorkspacePlanBillingIntervals
}) => string
export type GetWorkspacePlanProductId = (args: {
workspacePlan: WorkspacePricingProducts
}) => string
type Products = 'guest' | 'starter' | 'plus' | 'business' | 'team' | 'pro'
export type GetWorkspacePlanProductAndPriceIds = () => Omit<
Record<Products, { productId: string; monthly: string; yearly: string }>,
'team' | 'pro'
> & {
team?: { productId: string; monthly: string }
pro?: { productId: string; monthly: string; yearly: string }
}
export type SubscriptionDataInput = OverrideProperties<
SubscriptionData,
{
products: OverrideProperties<SubscriptionProduct, { subscriptionItemId?: string }>[]
}
>
export type ReconcileSubscriptionData = (args: {
subscriptionData: SubscriptionDataInput
prorationBehavior: 'always_invoice' | 'create_prorations' | 'none'
}) => Promise<void>
export const WorkspaceSeatType = <const>{
Viewer: 'viewer',
Editor: 'editor'
}
export type WorkspaceSeatType =
(typeof WorkspaceSeatType)[keyof typeof WorkspaceSeatType]
export type WorkspaceSeat = {
workspaceId: string
userId: string
type: WorkspaceSeatType
createdAt: Date
updatedAt: Date
}
// Prices
export type GetRecurringPrices = () => Promise<
{
id: string
currency: string
unitAmount: number
productId: string
}[]
>
export type GetWorkspacePlanProductPrices = () => Promise<WorkspacePlanProductPrices>
export type GetWorkspaceRolesAndSeats = (params: {
workspaceId: string
userIds?: string[]
}) => Promise<{
[userId: string]: {
role: WorkspaceAcl
seat: Nullable<WorkspaceSeat>
userId: string
}
}>
export type GetWorkspaceRoleAndSeat = (params: {
workspaceId: string
userId: string
}) => Promise<
| {
role: WorkspaceAcl
seat: Nullable<WorkspaceSeat>
userId: string
}
| undefined
>