bde148f286
* 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
168 lines
5.5 KiB
TypeScript
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
|
|
}))
|
|
}
|