Files
speckle-server/packages/server/modules/gatekeeper/domain/billing.ts
T
Kristaps Fabians Geikins d394e1cd9b fix: various presentations mode fixes related to resetting (#5635)
* better workspace feature flag ops

* user activity is correctly tracked

* more fixes
2025-10-03 12:39:59 +03:00

248 lines
6.6 KiB
TypeScript

import type {
Currency,
WorkspacePlanProductPrices,
WorkspacePricingProducts
} from '@/modules/gatekeeperCore/domain/billing'
import type { SubscriptionState } from '@/modules/gatekeeperCore/domain/events'
import type { Workspace, WorkspaceSeat } from '@/modules/workspacesCore/domain/types'
import { WorkspaceSeatType } from '@/modules/workspacesCore/domain/types'
import type {
Nullable,
Optional,
PaidWorkspacePlan,
PaidWorkspacePlans,
UnpaidWorkspacePlan,
WorkspacePlan,
WorkspacePlanBillingIntervals
} from '@speckle/shared'
import type Stripe from 'stripe'
import type { OverrideProperties } from 'type-fest'
import { z } from 'zod'
export { Currency } from '@/modules/gatekeeperCore/domain/billing'
export { type WorkspaceSeat, WorkspaceSeatType }
export type {
GetWorkspaceRoleAndSeat,
GetWorkspaceRolesAndSeats
} from '@/modules/workspacesCore/domain/operations'
export type GetWorkspacePlan = (
args:
| {
workspaceId: string
}
| { workspaceSlug: string }
) => Promise<WorkspacePlan | null>
export type GetWorkspacePlansByWorkspaceId = (args: {
workspaceIds: string[]
}) => Promise<Record<string, WorkspacePlan>>
export type GetWorkspaceWithPlan = (args: {
workspaceId: string
}) => Promise<Optional<Workspace & { plan: Nullable<WorkspacePlan> }>>
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
userId: string
workspacePlan: PaidWorkspacePlans
paymentStatus: SessionPaymentStatus
billingInterval: WorkspacePlanBillingIntervals
currency: Currency
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>
export type CreateCheckoutSession = (args: {
workspaceId: string
userId: string
workspaceSlug: string
editorsCount: number
workspacePlan: PaidWorkspacePlans
billingInterval: WorkspacePlanBillingIntervals
isCreateFlow: boolean
currency: Currency
}) => Promise<CheckoutSession>
export type WorkspaceSubscription = {
workspaceId: string
createdAt: Date
updatedAt: Date
currentBillingCycleEnd: Date
billingInterval: WorkspacePlanBillingIntervals
currency: Currency
updateIntent: SubscriptionUpdateIntent | null
subscriptionData: SubscriptionData
}
export type SubscriptionUpdateIntent = {
userId: string
products: SubscriptionIntentProduct[]
planName: PaidWorkspacePlans
} & Pick<
WorkspaceSubscription,
// status is not needed cause its always provided by stripe
'currentBillingCycleEnd' | 'currency' | 'billingInterval' | 'updatedAt'
>
const subscriptionProduct = z.object({
productId: z.string(),
subscriptionItemId: z.string(), // does not exist until billing is called with success
priceId: z.string(),
quantity: z.number()
})
export type SubscriptionProduct = z.infer<typeof subscriptionProduct>
type SubscriptionIntentProduct = Pick<
SubscriptionProduct,
'productId' | 'priceId' | 'quantity'
>
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'), // TODO: Should we get rid of trial related states?
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
}: {
subscriptionData: SubscriptionData
}): number => {
const product = subscriptionData.products[0]
return product?.quantity || 0
}
export const getSubscriptionState = (
subscription: WorkspaceSubscription
): SubscriptionState => ({
billingInterval: subscription.billingInterval,
totalEditorSeats: calculateSubscriptionSeats({
subscriptionData: subscription.subscriptionData
})
})
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 GetStripeClient = () => Stripe
export type GetSubscriptionData = (args: {
subscriptionId: string
}) => Promise<SubscriptionData>
export type GetWorkspacePlanPriceId = (args: {
workspacePlan: WorkspacePricingProducts
billingInterval: WorkspacePlanBillingIntervals
currency: Currency
}) => string
export type GetWorkspacePlanProductId = (args: {
workspacePlan: WorkspacePricingProducts
}) => string
export type MultiCurrencyPrice = {
usd: string
gbp: string
}
type MultiCurrencyProductPrice = {
monthly: MultiCurrencyPrice
yearly: MultiCurrencyPrice
}
export type WorkspacePlanProductAndPriceIds = Record<
PaidWorkspacePlans,
{ productId: string } & MultiCurrencyProductPrice
>
export type GetWorkspacePlanProductAndPriceIds = () => WorkspacePlanProductAndPriceIds
export type SubscriptionDataInput = OverrideProperties<
SubscriptionData,
{
products: OverrideProperties<SubscriptionProduct, { subscriptionItemId?: string }>[]
}
>
export type ReconcileSubscriptionData = (args: {
subscriptionData: SubscriptionDataInput
prorationBehavior: 'always_invoice' | 'create_prorations' | 'none'
}) => Promise<void>
// Prices
export type GetRecurringPrices = () => Promise<
{
id: string
currency: string
unitAmount: number
productId: string
}[]
>
export type GetWorkspacePlanProductPrices = () => Promise<WorkspacePlanProductPrices>