import { Currency, WorkspacePlanProductPrices, WorkspacePricingProducts } from '@/modules/gatekeeperCore/domain/billing' import { SubscriptionState } from '@/modules/gatekeeperCore/domain/events' import { Workspace, WorkspaceSeat, WorkspaceSeatType } from '@/modules/workspacesCore/domain/types' import { Nullable, Optional, PaidWorkspacePlan, PaidWorkspacePlans, UnpaidWorkspacePlan, WorkspacePlan, WorkspacePlanBillingIntervals } from '@speckle/shared' import type Stripe from 'stripe' import { 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 }) => Promise export type GetWorkspacePlansByWorkspaceId = (args: { workspaceIds: string[] }) => Promise> export type GetWorkspaceWithPlan = (args: { workspaceId: string }) => Promise }>> export type UpsertPaidWorkspacePlan = (args: { workspacePlan: PaidWorkspacePlan }) => Promise export type UpsertUnpaidWorkspacePlan = (args: { workspacePlan: UnpaidWorkspacePlan }) => Promise export type UpsertWorkspacePlan = (args: { workspacePlan: WorkspacePlan }) => Promise 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 export type GetCheckoutSession = (args: { sessionId: string }) => Promise export type DeleteCheckoutSession = (args: { checkoutSessionId: string }) => Promise export type GetWorkspaceCheckoutSession = (args: { workspaceId: string }) => Promise export type UpdateCheckoutSessionStatus = (args: { sessionId: string paymentStatus: SessionPaymentStatus }) => Promise export type CreateCheckoutSession = (args: { workspaceId: string userId: string workspaceSlug: string editorsCount: number workspacePlan: PaidWorkspacePlans billingInterval: WorkspacePlanBillingIntervals isCreateFlow: boolean currency: Currency }) => Promise 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 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 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 export type GetWorkspaceSubscription = (args: { workspaceId: string }) => Promise export type GetWorkspaceSubscriptions = () => Promise export type GetWorkspaceSubscriptionBySubscriptionId = (args: { subscriptionId: string }) => Promise export type GetStripeClient = () => Stripe export type GetSubscriptionData = (args: { subscriptionId: string }) => Promise 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[] } > export type ReconcileSubscriptionData = (args: { subscriptionData: SubscriptionDataInput prorationBehavior: 'always_invoice' | 'create_prorations' | 'none' }) => Promise // Prices export type GetRecurringPrices = () => Promise< { id: string currency: string unitAmount: number productId: string }[] > export type GetWorkspacePlanProductPrices = () => Promise