Files
speckle-server/packages/server/modules/gatekeeper/clients/stripe.ts
T
Kristaps Fabians Geikins bde148f286 chore(server): migrating fully to ESM (#5042)
* wip

* some extra fixes

* stuff kinda works?

* need to figure out mocks

* need to figure out mocks

* fix db listener

* gqlgen fix

* minor gqlgen watch adjustment

* lint fixes

* delete old codegen file

* converting migrations to ESM

* getModuleDIrectory

* vitest sort of works

* added back ts-vitest

* resolve gql double load

* fixing test timeout configs

* TSC lint fix

* fix automate tests

* moar debugging

* debugging

* more debugging

* codegen update

* server works

* yargs migrated

* chore(server): getting rid of global mocks for Server ESM (#5046)

* got rid of email mock

* got rid of comment mocks

* got rid of multi region mocks

* got rid of stripe mock

* admin override mock updated

* removed final mock

* fixing import.meta.resolve calls

* another import.meta.resolve fix

* added requested test

* nyc ESM fix

* removed unneeded deps + linting

* yarn lock forgot to commit

* tryna fix flakyness

* email capture util fix

* sendEmail fix

* fix TSX check

* sender transporter fix + CR comments

* merge main fix

* test fixx

* circleci fix

* gqlgen bigint fix

* error formatter fix

* more error formatting improvements

* esmloader added to Dockerfile

* more dockerfile fixes

* bg jobs fix
2025-07-14 10:26:19 +03:00

168 lines
5.5 KiB
TypeScript

/* eslint-disable camelcase */
import { getResultUrl } from '@/modules/gatekeeper/clients/getResultUrl'
import {
GetRecurringPrices,
GetStripeClient,
GetSubscriptionData,
ReconcileSubscriptionData,
SubscriptionData
} from '@/modules/gatekeeper/domain/billing'
import { LogicError, TestOnlyLogicError } from '@/modules/shared/errors'
import { getStripeApiKey, isTestEnv } from '@/modules/shared/helpers/envHelper'
import { TIME_MS } from '@speckle/shared'
import { isString } from 'lodash-es'
import { Stripe } from 'stripe'
let stripeClient: Stripe | undefined = undefined
export const getStripeClient: GetStripeClient = () => {
if (!stripeClient) stripeClient = new Stripe(getStripeApiKey(), { typescript: true })
return stripeClient
}
export const setStripeClient = (client: Stripe | undefined) => {
if (!isTestEnv()) {
throw new TestOnlyLogicError()
}
stripeClient = client
}
export const createCustomerPortalUrlFactory =
({
getStripeClient,
frontendOrigin
}: {
getStripeClient: GetStripeClient
frontendOrigin: string
}) =>
async ({
workspaceId,
workspaceSlug,
customerId
}: {
customerId: string
workspaceId: string
workspaceSlug: string
}): Promise<string> => {
const session = await getStripeClient().billingPortal.sessions.create({
customer: customerId,
return_url: getResultUrl({
frontendOrigin,
workspaceId,
workspaceSlug
}).toString()
})
return session.url
}
export const getStripeSubscriptionDataFactory =
({ getStripeClient }: { getStripeClient: GetStripeClient }): GetSubscriptionData =>
async ({ subscriptionId }) => {
const stripeSubscription = await getStripeClient().subscriptions.retrieve(
subscriptionId
)
return parseSubscriptionData(stripeSubscription)
}
export const parseSubscriptionData = (
stripeSubscription: Stripe.Subscription
): SubscriptionData => {
const subscriptionData = {
customerId:
typeof stripeSubscription.customer === 'string'
? stripeSubscription.customer
: stripeSubscription.customer.id,
subscriptionId: stripeSubscription.id,
status: stripeSubscription.status,
cancelAt: stripeSubscription.cancel_at
? new Date(stripeSubscription.cancel_at * TIME_MS.second)
: null,
currentPeriodEnd: stripeSubscription.current_period_end * TIME_MS.second, // this value arrives as a UNIX timestamp
products: stripeSubscription.items.data.map((subscriptionItem) => {
const productId =
typeof subscriptionItem.price.product === 'string'
? subscriptionItem.price.product
: subscriptionItem.price.product.id
const quantity = subscriptionItem.quantity
if (!quantity)
throw new LogicError(
'invalid subscription, we do not support products without quantities'
)
return {
priceId: subscriptionItem.price.id,
productId,
quantity,
subscriptionItemId: subscriptionItem.id
}
})
}
return SubscriptionData.parse(subscriptionData)
}
// this should be a reconcile subscriptions, we keep an accurate state in the DB
// on each change, we're reconciling that state to stripe
export const reconcileWorkspaceSubscriptionFactory =
({
getStripeClient,
getStripeSubscriptionData
}: {
getStripeClient: GetStripeClient
getStripeSubscriptionData: GetSubscriptionData
}): ReconcileSubscriptionData =>
async ({ subscriptionData, prorationBehavior }) => {
const existingSubscriptionState = await getStripeSubscriptionData({
subscriptionId: subscriptionData.subscriptionId
})
const items: Stripe.SubscriptionUpdateParams.Item[] = []
for (const product of subscriptionData.products) {
const existingProduct = existingSubscriptionState.products.find(
(p) => p.productId === product.productId
)
// we're adding a new product to the sub
if (!existingProduct) {
items.push({ quantity: product.quantity, price: product.priceId })
// we're moving a product to a new price for ie upgrading to a yearly plan
} else if (existingProduct.priceId !== product.priceId) {
items.push({ quantity: product.quantity, price: product.priceId })
items.push({ id: existingProduct.subscriptionItemId, deleted: true })
} else {
items.push({
quantity: product.quantity,
id: existingProduct.subscriptionItemId
})
}
}
// remove products from the sub
const productIds = subscriptionData.products.map((p) => p.productId)
const removedProducts = existingSubscriptionState.products.filter(
(p) => !productIds.includes(p.productId)
)
for (const removedProduct of removedProducts) {
items.push({ id: removedProduct.subscriptionItemId, deleted: true })
}
// workspaceSubscription.subscriptionData.products.
// const item = workspaceSubscription.subscriptionData.products.find(p => p.)
await getStripeClient().subscriptions.update(subscriptionData.subscriptionId, {
items,
proration_behavior: prorationBehavior
})
}
export const getRecurringPricesFactory =
(deps: { getStripeClient: GetStripeClient }): GetRecurringPrices =>
async () => {
const results = await deps.getStripeClient().prices.list({
type: 'recurring',
limit: 100,
active: true
})
return results.data.map((p) => ({
id: p.id,
currency: p.currency,
unitAmount: p.unit_amount!,
productId: isString(p.product) ? p.product : p.product.id
}))
}