Merge pull request #4236 from specklesystems/alessandro/web-2803-downscale-workspace-subscription
Alessandro/web 2803 downscale workspace subscription
This commit is contained in:
@@ -66,6 +66,7 @@ export const parseSubscriptionData = (
|
||||
cancelAt: stripeSubscription.cancel_at
|
||||
? new Date(stripeSubscription.cancel_at * 1000)
|
||||
: null,
|
||||
currentPeriodEnd: stripeSubscription.current_period_end * 1000, // this value arrives as a UNIX timestamp
|
||||
products: stripeSubscription.items.data.map((subscriptionItem) => {
|
||||
const productId =
|
||||
typeof subscriptionItem.price.product === 'string'
|
||||
@@ -84,7 +85,7 @@ export const parseSubscriptionData = (
|
||||
}
|
||||
})
|
||||
}
|
||||
return subscriptionData
|
||||
return SubscriptionData.parse(subscriptionData)
|
||||
}
|
||||
|
||||
// this should be a reconcile subscriptions, we keep an accurate state in the DB
|
||||
|
||||
@@ -113,7 +113,7 @@ const subscriptionProduct = z.object({
|
||||
|
||||
export type SubscriptionProduct = z.infer<typeof subscriptionProduct>
|
||||
|
||||
export const subscriptionData = z.object({
|
||||
export const SubscriptionData = z.object({
|
||||
subscriptionId: z.string().min(1),
|
||||
customerId: z.string().min(1),
|
||||
cancelAt: z.date().nullable(),
|
||||
@@ -127,8 +127,11 @@ export const subscriptionData = z.object({
|
||||
z.literal('unpaid'),
|
||||
z.literal('paused')
|
||||
]),
|
||||
products: subscriptionProduct.array()
|
||||
products: subscriptionProduct.array(),
|
||||
currentPeriodEnd: z.coerce.date()
|
||||
})
|
||||
// this abstracts the stripe sub data
|
||||
export type SubscriptionData = z.infer<typeof SubscriptionData>
|
||||
|
||||
export const calculateSubscriptionSeats = ({
|
||||
subscriptionData,
|
||||
@@ -147,9 +150,6 @@ export const calculateSubscriptionSeats = ({
|
||||
return { guest: guestProduct?.quantity || 0, plan: planProduct?.quantity || 0 }
|
||||
}
|
||||
|
||||
// this abstracts the stripe sub data
|
||||
export type SubscriptionData = z.infer<typeof subscriptionData>
|
||||
|
||||
export type UpsertWorkspaceSubscription = (args: {
|
||||
workspaceSubscription: WorkspaceSubscription
|
||||
}) => Promise<void>
|
||||
|
||||
@@ -14,23 +14,23 @@ import {
|
||||
acquireTaskLockFactory,
|
||||
releaseTaskLockFactory
|
||||
} from '@/modules/core/repositories/scheduledTasks'
|
||||
import {
|
||||
downscaleWorkspaceSubscriptionFactory,
|
||||
manageSubscriptionDownscaleFactory
|
||||
} from '@/modules/gatekeeper/services/subscriptions'
|
||||
import {
|
||||
changeExpiredTrialWorkspacePlanStatusesFactory,
|
||||
getWorkspacePlanByProjectIdFactory,
|
||||
getWorkspacePlanFactory,
|
||||
getWorkspacesByPlanAgeFactory,
|
||||
getWorkspaceSubscriptionsPastBillingCycleEndFactory,
|
||||
getWorkspaceSubscriptionsPastBillingCycleEndFactoryNewPlans,
|
||||
getWorkspaceSubscriptionsPastBillingCycleEndFactoryOldPlans,
|
||||
upsertWorkspaceSubscriptionFactory
|
||||
} from '@/modules/gatekeeper/repositories/billing'
|
||||
import {
|
||||
countWorkspaceRoleWithOptionalProjectRoleFactory,
|
||||
getWorkspaceCollaboratorsFactory
|
||||
} from '@/modules/workspaces/repositories/workspaces'
|
||||
import { reconcileWorkspaceSubscriptionFactory } from '@/modules/gatekeeper/clients/stripe'
|
||||
import {
|
||||
getSubscriptionDataFactory,
|
||||
reconcileWorkspaceSubscriptionFactory
|
||||
} from '@/modules/gatekeeper/clients/stripe'
|
||||
import { ScheduleExecution } from '@/modules/core/domain/scheduledTasks/operations'
|
||||
import { EventBusEmit, getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { sendWorkspaceTrialExpiresEmailFactory } from '@/modules/gatekeeper/services/trialEmails'
|
||||
@@ -42,6 +42,13 @@ import coreModule from '@/modules/core/index'
|
||||
import { isProjectReadOnlyFactory } from '@/modules/gatekeeper/services/readOnly'
|
||||
import { WorkspaceReadOnlyError } from '@/modules/gatekeeper/errors/billing'
|
||||
import { InvalidLicenseError } from '@/modules/gatekeeper/errors/license'
|
||||
import {
|
||||
downscaleWorkspaceSubscriptionFactoryNew,
|
||||
downscaleWorkspaceSubscriptionFactoryOld,
|
||||
manageSubscriptionDownscaleFactoryNew,
|
||||
manageSubscriptionDownscaleFactoryOld
|
||||
} from '@/modules/gatekeeper/services/subscriptions/manageSubscriptionDownscale'
|
||||
import { countSeatsByTypeInWorkspaceFactory } from '@/modules/gatekeeper/repositories/workspaceSeat'
|
||||
|
||||
const { FF_GATEKEEPER_MODULE_ENABLED, FF_BILLING_INTEGRATION_ENABLED } =
|
||||
getFeatureFlags()
|
||||
@@ -58,16 +65,31 @@ const scheduleWorkspaceSubscriptionDownscale = ({
|
||||
}) => {
|
||||
const stripe = getStripeClient()
|
||||
|
||||
const manageSubscriptionDownscale = manageSubscriptionDownscaleFactory({
|
||||
downscaleWorkspaceSubscription: downscaleWorkspaceSubscriptionFactory({
|
||||
const manageSubscriptionDownscaleOld = manageSubscriptionDownscaleFactoryOld({
|
||||
downscaleWorkspaceSubscription: downscaleWorkspaceSubscriptionFactoryOld({
|
||||
countWorkspaceRole: countWorkspaceRoleWithOptionalProjectRoleFactory({ db }),
|
||||
getWorkspacePlan: getWorkspacePlanFactory({ db }),
|
||||
reconcileSubscriptionData: reconcileWorkspaceSubscriptionFactory({ stripe }),
|
||||
getWorkspacePlanProductId
|
||||
}),
|
||||
getWorkspaceSubscriptions: getWorkspaceSubscriptionsPastBillingCycleEndFactory({
|
||||
db
|
||||
getWorkspaceSubscriptions:
|
||||
getWorkspaceSubscriptionsPastBillingCycleEndFactoryOldPlans({
|
||||
db
|
||||
}),
|
||||
updateWorkspaceSubscription: upsertWorkspaceSubscriptionFactory({ db })
|
||||
})
|
||||
const manageSubscriptionDownscaleNew = manageSubscriptionDownscaleFactoryNew({
|
||||
downscaleWorkspaceSubscription: downscaleWorkspaceSubscriptionFactoryNew({
|
||||
countSeatsByTypeInWorkspace: countSeatsByTypeInWorkspaceFactory({ db }),
|
||||
getWorkspacePlan: getWorkspacePlanFactory({ db }),
|
||||
reconcileSubscriptionData: reconcileWorkspaceSubscriptionFactory({ stripe }),
|
||||
getWorkspacePlanProductId
|
||||
}),
|
||||
getWorkspaceSubscriptions:
|
||||
getWorkspaceSubscriptionsPastBillingCycleEndFactoryNewPlans({
|
||||
db
|
||||
}),
|
||||
getSubscriptionData: getSubscriptionDataFactory({ stripe }),
|
||||
updateWorkspaceSubscription: upsertWorkspaceSubscriptionFactory({ db })
|
||||
})
|
||||
|
||||
@@ -76,7 +98,10 @@ const scheduleWorkspaceSubscriptionDownscale = ({
|
||||
cronExpression,
|
||||
'WorkspaceSubscriptionDownscale',
|
||||
async (_scheduledTime, { logger }) => {
|
||||
await manageSubscriptionDownscale({ logger })
|
||||
await Promise.all([
|
||||
manageSubscriptionDownscaleOld({ logger }), // Only takes old plans subscriptions
|
||||
manageSubscriptionDownscaleNew({ logger }) // Only takes new plans subscriptions
|
||||
])
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import { WorkspacePlan } from '@/modules/gatekeeperCore/domain/billing'
|
||||
import { formatJsonArrayRecords } from '@/modules/shared/helpers/dbHelper'
|
||||
import { Workspace } from '@/modules/workspacesCore/domain/types'
|
||||
import { Workspaces } from '@/modules/workspacesCore/helpers/db'
|
||||
import { PaidWorkspacePlansNew, PaidWorkspacePlansOld } from '@speckle/shared'
|
||||
import { Knex } from 'knex'
|
||||
import { omit } from 'lodash'
|
||||
|
||||
@@ -37,6 +38,14 @@ const WorkspacePlans = buildTableHelper('workspace_plans', [
|
||||
'createdAt',
|
||||
'updatedAt'
|
||||
])
|
||||
const WorkspaceSubscriptions = buildTableHelper('workspace_subscriptions', [
|
||||
'workspaceId',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'currentBillingCycleEnd',
|
||||
'billingInterval',
|
||||
'subscriptionData'
|
||||
])
|
||||
|
||||
const tables = {
|
||||
workspaces: (db: Knex) => db<Workspace>('workspaces'),
|
||||
@@ -212,15 +221,41 @@ export const getWorkspaceSubscriptionBySubscriptionIdFactory =
|
||||
return subscription ?? null
|
||||
}
|
||||
|
||||
export const getWorkspaceSubscriptionsPastBillingCycleEndFactory =
|
||||
const newPlans = Object.values(PaidWorkspacePlansNew)
|
||||
const oldPlans = Object.values(PaidWorkspacePlansOld)
|
||||
|
||||
export const getWorkspaceSubscriptionsPastBillingCycleEndFactoryOldPlans =
|
||||
({ db }: { db: Knex }): GetWorkspaceSubscriptions =>
|
||||
async () => {
|
||||
const cycleEnd = new Date()
|
||||
cycleEnd.setMinutes(cycleEnd.getMinutes() + 5)
|
||||
return await tables
|
||||
.workspaceSubscriptions(db)
|
||||
.select()
|
||||
.join(
|
||||
WorkspacePlans.name,
|
||||
WorkspacePlans.col.workspaceId,
|
||||
'workspace_subscriptions.workspaceId'
|
||||
)
|
||||
.whereIn(WorkspacePlans.col.name, oldPlans)
|
||||
.where('currentBillingCycleEnd', '<', cycleEnd)
|
||||
.select(WorkspaceSubscriptions.cols)
|
||||
}
|
||||
|
||||
export const getWorkspaceSubscriptionsPastBillingCycleEndFactoryNewPlans =
|
||||
({ db }: { db: Knex }): GetWorkspaceSubscriptions =>
|
||||
async () => {
|
||||
const cycleEnd = new Date()
|
||||
cycleEnd.setMinutes(cycleEnd.getMinutes() + 5)
|
||||
return await tables
|
||||
.workspaceSubscriptions(db)
|
||||
.join(
|
||||
WorkspacePlans.name,
|
||||
WorkspacePlans.col.workspaceId,
|
||||
'workspace_subscriptions.workspaceId'
|
||||
)
|
||||
.whereIn(WorkspacePlans.col.name, newPlans)
|
||||
.where('currentBillingCycleEnd', '<', cycleEnd)
|
||||
.select(WorkspaceSubscriptions.cols)
|
||||
}
|
||||
|
||||
export const getWorkspacePlanByProjectIdFactory =
|
||||
|
||||
@@ -22,6 +22,7 @@ import { withTransaction } from '@/modules/shared/helpers/dbHelper'
|
||||
import { getStripeClient } from '@/modules/gatekeeper/stripe'
|
||||
import { handleSubscriptionUpdateFactory } from '@/modules/gatekeeper/services/subscriptions'
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import { SubscriptionData } from '@/modules/gatekeeper/domain/billing'
|
||||
|
||||
export const getBillingRouter = (): Router => {
|
||||
const router = Router()
|
||||
@@ -144,6 +145,19 @@ export const getBillingRouter = (): Router => {
|
||||
})({ subscriptionData: parseSubscriptionData(event.data.object) })
|
||||
|
||||
break
|
||||
case 'invoice.created':
|
||||
const subscriptionData = await getSubscriptionFromEventFactory({ stripe })(
|
||||
event
|
||||
)
|
||||
if (!subscriptionData) break
|
||||
await handleSubscriptionUpdateFactory({
|
||||
getWorkspacePlan: getWorkspacePlanFactory({ db }),
|
||||
upsertPaidWorkspacePlan: upsertPaidWorkspacePlanFactory({ db }),
|
||||
getWorkspaceSubscriptionBySubscriptionId:
|
||||
getWorkspaceSubscriptionBySubscriptionIdFactory({ db }),
|
||||
upsertWorkspaceSubscription: upsertWorkspaceSubscriptionFactory({ db })
|
||||
})({ subscriptionData })
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
@@ -154,3 +168,18 @@ export const getBillingRouter = (): Router => {
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
const getSubscriptionFromEventFactory =
|
||||
({ stripe }: { stripe: Stripe }) =>
|
||||
async (event: Stripe.InvoiceCreatedEvent): Promise<SubscriptionData | null> => {
|
||||
const subscription = event.data.object.subscription
|
||||
if (!subscription) {
|
||||
return null
|
||||
}
|
||||
if (typeof subscription === 'string') {
|
||||
return await getSubscriptionDataFactory({ stripe })({
|
||||
subscriptionId: subscription
|
||||
})
|
||||
}
|
||||
return parseSubscriptionData(subscription)
|
||||
}
|
||||
|
||||
@@ -66,18 +66,7 @@ export const completeCheckoutSessionFactory =
|
||||
const subscriptionData = await getSubscriptionData({
|
||||
subscriptionId
|
||||
})
|
||||
const currentBillingCycleEnd = new Date()
|
||||
switch (checkoutSession.billingInterval) {
|
||||
case 'monthly':
|
||||
currentBillingCycleEnd.setMonth(currentBillingCycleEnd.getMonth() + 1)
|
||||
break
|
||||
case 'yearly':
|
||||
currentBillingCycleEnd.setMonth(currentBillingCycleEnd.getMonth() + 12)
|
||||
break
|
||||
|
||||
default:
|
||||
throwUncoveredError(checkoutSession.billingInterval)
|
||||
}
|
||||
const currentBillingCycleEnd = subscriptionData.currentPeriodEnd
|
||||
|
||||
const workspaceSubscription = {
|
||||
createdAt: new Date(),
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import type { Logger } from '@/observability/logging'
|
||||
import {
|
||||
GetWorkspacePlan,
|
||||
GetWorkspacePlanPriceId,
|
||||
GetWorkspacePlanProductId,
|
||||
GetWorkspaceSubscription,
|
||||
GetWorkspaceSubscriptionBySubscriptionId,
|
||||
GetWorkspaceSubscriptions,
|
||||
ReconcileSubscriptionData,
|
||||
SubscriptionData,
|
||||
SubscriptionDataInput,
|
||||
UpsertPaidWorkspacePlan,
|
||||
UpsertWorkspaceSubscription,
|
||||
WorkspaceSeatType,
|
||||
WorkspaceSubscription
|
||||
WorkspaceSeatType
|
||||
} from '@/modules/gatekeeper/domain/billing'
|
||||
import {
|
||||
WorkspacePlanMismatchError,
|
||||
@@ -27,9 +24,7 @@ import {
|
||||
throwUncoveredError,
|
||||
WorkspaceRoles
|
||||
} from '@speckle/shared'
|
||||
import { cloneDeep, isEqual, sum } from 'lodash'
|
||||
import { mutateSubscriptionDataWithNewValidSeatNumbers } from '@/modules/gatekeeper/services/subscriptions/mutateSubscriptionDataWithNewValidSeatNumbers'
|
||||
import { calculateNewBillingCycleEnd } from '@/modules/gatekeeper/services/subscriptions/calculateNewBillingCycleEnd'
|
||||
import { cloneDeep, sum } from 'lodash'
|
||||
import { CountSeatsByTypeInWorkspace } from '@/modules/gatekeeper/domain/operations'
|
||||
|
||||
export const handleSubscriptionUpdateFactory =
|
||||
@@ -297,117 +292,3 @@ export const addWorkspaceSubscriptionSeatIfNeededFactoryOld =
|
||||
prorationBehavior: 'create_prorations'
|
||||
})
|
||||
}
|
||||
|
||||
type DownscaleWorkspaceSubscription = (args: {
|
||||
workspaceSubscription: WorkspaceSubscription
|
||||
}) => Promise<boolean>
|
||||
|
||||
export const downscaleWorkspaceSubscriptionFactory =
|
||||
({
|
||||
getWorkspacePlan,
|
||||
countWorkspaceRole,
|
||||
getWorkspacePlanProductId,
|
||||
reconcileSubscriptionData
|
||||
}: {
|
||||
getWorkspacePlan: GetWorkspacePlan
|
||||
countWorkspaceRole: CountWorkspaceRoleWithOptionalProjectRole
|
||||
getWorkspacePlanProductId: GetWorkspacePlanProductId
|
||||
reconcileSubscriptionData: ReconcileSubscriptionData
|
||||
}): DownscaleWorkspaceSubscription =>
|
||||
async ({ workspaceSubscription }) => {
|
||||
const workspaceId = workspaceSubscription.workspaceId
|
||||
|
||||
const workspacePlan = await getWorkspacePlan({ workspaceId })
|
||||
if (!workspacePlan) throw new WorkspacePlanNotFoundError()
|
||||
|
||||
switch (workspacePlan.name) {
|
||||
case 'team':
|
||||
case 'pro':
|
||||
// Cause seat types matter, a future issue
|
||||
throw new NotImplementedError()
|
||||
case 'starter':
|
||||
case 'plus':
|
||||
case 'business':
|
||||
break
|
||||
case 'unlimited':
|
||||
case 'academia':
|
||||
case 'starterInvoiced':
|
||||
case 'plusInvoiced':
|
||||
case 'businessInvoiced':
|
||||
case 'free':
|
||||
throw new WorkspacePlanMismatchError()
|
||||
default:
|
||||
throwUncoveredError(workspacePlan)
|
||||
}
|
||||
|
||||
if (workspacePlan.status === 'canceled') return false
|
||||
|
||||
// TODO: Guests will be able to have a paid seat
|
||||
const [guestCount, memberCount, adminCount] = await Promise.all([
|
||||
countWorkspaceRole({ workspaceId, workspaceRole: 'workspace:guest' }),
|
||||
countWorkspaceRole({ workspaceId, workspaceRole: 'workspace:member' }),
|
||||
countWorkspaceRole({ workspaceId, workspaceRole: 'workspace:admin' })
|
||||
])
|
||||
|
||||
const subscriptionData = cloneDeep(workspaceSubscription.subscriptionData)
|
||||
|
||||
mutateSubscriptionDataWithNewValidSeatNumbers({
|
||||
seatCount: guestCount,
|
||||
workspacePlan: 'guest',
|
||||
getWorkspacePlanProductId,
|
||||
subscriptionData
|
||||
})
|
||||
mutateSubscriptionDataWithNewValidSeatNumbers({
|
||||
seatCount: memberCount + adminCount,
|
||||
workspacePlan: workspacePlan.name,
|
||||
getWorkspacePlanProductId,
|
||||
subscriptionData
|
||||
})
|
||||
|
||||
if (!isEqual(subscriptionData, workspaceSubscription.subscriptionData)) {
|
||||
await reconcileSubscriptionData({ subscriptionData, prorationBehavior: 'none' })
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const manageSubscriptionDownscaleFactory =
|
||||
({
|
||||
getWorkspaceSubscriptions,
|
||||
downscaleWorkspaceSubscription,
|
||||
updateWorkspaceSubscription
|
||||
}: {
|
||||
getWorkspaceSubscriptions: GetWorkspaceSubscriptions
|
||||
downscaleWorkspaceSubscription: DownscaleWorkspaceSubscription
|
||||
updateWorkspaceSubscription: UpsertWorkspaceSubscription
|
||||
}) =>
|
||||
async (context: { logger: Logger }) => {
|
||||
const { logger } = context
|
||||
const subscriptions = await getWorkspaceSubscriptions()
|
||||
for (const workspaceSubscription of subscriptions) {
|
||||
const log = logger.child({ workspaceId: workspaceSubscription.workspaceId })
|
||||
try {
|
||||
const subDownscaled = await downscaleWorkspaceSubscription({
|
||||
workspaceSubscription
|
||||
})
|
||||
if (subDownscaled) {
|
||||
log.info(
|
||||
'Downscaled workspace subscription to match the current workspace team'
|
||||
)
|
||||
} else {
|
||||
log.info('Did not need to downscale the workspace subscription')
|
||||
}
|
||||
} catch (err) {
|
||||
log.error({ err }, 'Failed to downscale workspace subscription')
|
||||
}
|
||||
const newBillingCycleEnd = calculateNewBillingCycleEnd({ workspaceSubscription })
|
||||
const updatedWorkspaceSubscription = {
|
||||
...workspaceSubscription,
|
||||
currentBillingCycleEnd: newBillingCycleEnd
|
||||
}
|
||||
await updateWorkspaceSubscription({
|
||||
workspaceSubscription: updatedWorkspaceSubscription
|
||||
})
|
||||
log.info({ updatedWorkspaceSubscription }, 'Updated workspace billing cycle end')
|
||||
}
|
||||
}
|
||||
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
import {
|
||||
GetSubscriptionData,
|
||||
GetWorkspacePlan,
|
||||
GetWorkspacePlanProductId,
|
||||
GetWorkspaceSubscriptions,
|
||||
ReconcileSubscriptionData,
|
||||
UpsertWorkspaceSubscription,
|
||||
WorkspaceSubscription
|
||||
} from '@/modules/gatekeeper/domain/billing'
|
||||
import { CountSeatsByTypeInWorkspace } from '@/modules/gatekeeper/domain/operations'
|
||||
import {
|
||||
WorkspacePlanMismatchError,
|
||||
WorkspacePlanNotFoundError
|
||||
} from '@/modules/gatekeeper/errors/billing'
|
||||
import { calculateNewBillingCycleEnd } from '@/modules/gatekeeper/services/subscriptions/calculateNewBillingCycleEnd'
|
||||
import { mutateSubscriptionDataWithNewValidSeatNumbers } from '@/modules/gatekeeper/services/subscriptions/mutateSubscriptionDataWithNewValidSeatNumbers'
|
||||
import { NotImplementedError } from '@/modules/shared/errors'
|
||||
import { CountWorkspaceRoleWithOptionalProjectRole } from '@/modules/workspaces/domain/operations'
|
||||
import { Logger } from '@/observability/logging'
|
||||
import { throwUncoveredError } from '@speckle/shared'
|
||||
import { cloneDeep, isEqual } from 'lodash'
|
||||
|
||||
type DownscaleWorkspaceSubscription = (args: {
|
||||
workspaceSubscription: WorkspaceSubscription
|
||||
}) => Promise<boolean>
|
||||
|
||||
export const downscaleWorkspaceSubscriptionFactoryOld =
|
||||
({
|
||||
getWorkspacePlan,
|
||||
countWorkspaceRole,
|
||||
getWorkspacePlanProductId,
|
||||
reconcileSubscriptionData
|
||||
}: {
|
||||
getWorkspacePlan: GetWorkspacePlan
|
||||
countWorkspaceRole: CountWorkspaceRoleWithOptionalProjectRole
|
||||
getWorkspacePlanProductId: GetWorkspacePlanProductId
|
||||
reconcileSubscriptionData: ReconcileSubscriptionData
|
||||
}): DownscaleWorkspaceSubscription =>
|
||||
async ({ workspaceSubscription }) => {
|
||||
const workspaceId = workspaceSubscription.workspaceId
|
||||
|
||||
const workspacePlan = await getWorkspacePlan({ workspaceId })
|
||||
if (!workspacePlan) throw new WorkspacePlanNotFoundError()
|
||||
|
||||
switch (workspacePlan.name) {
|
||||
case 'team':
|
||||
case 'pro':
|
||||
// Cause seat types matter, a future issue
|
||||
throw new NotImplementedError()
|
||||
case 'starter':
|
||||
case 'plus':
|
||||
case 'business':
|
||||
break
|
||||
case 'unlimited':
|
||||
case 'academia':
|
||||
case 'starterInvoiced':
|
||||
case 'plusInvoiced':
|
||||
case 'businessInvoiced':
|
||||
case 'free':
|
||||
throw new WorkspacePlanMismatchError()
|
||||
default:
|
||||
throwUncoveredError(workspacePlan)
|
||||
}
|
||||
|
||||
if (workspacePlan.status === 'canceled') return false
|
||||
|
||||
// TODO: Guests will be able to have a paid seat
|
||||
const [guestCount, memberCount, adminCount] = await Promise.all([
|
||||
countWorkspaceRole({ workspaceId, workspaceRole: 'workspace:guest' }),
|
||||
countWorkspaceRole({ workspaceId, workspaceRole: 'workspace:member' }),
|
||||
countWorkspaceRole({ workspaceId, workspaceRole: 'workspace:admin' })
|
||||
])
|
||||
|
||||
const subscriptionData = cloneDeep(workspaceSubscription.subscriptionData)
|
||||
|
||||
mutateSubscriptionDataWithNewValidSeatNumbers({
|
||||
seatCount: guestCount,
|
||||
workspacePlan: 'guest',
|
||||
getWorkspacePlanProductId,
|
||||
subscriptionData
|
||||
})
|
||||
mutateSubscriptionDataWithNewValidSeatNumbers({
|
||||
seatCount: memberCount + adminCount,
|
||||
workspacePlan: workspacePlan.name,
|
||||
getWorkspacePlanProductId,
|
||||
subscriptionData
|
||||
})
|
||||
|
||||
if (!isEqual(subscriptionData, workspaceSubscription.subscriptionData)) {
|
||||
await reconcileSubscriptionData({ subscriptionData, prorationBehavior: 'none' })
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const downscaleWorkspaceSubscriptionFactoryNew =
|
||||
({
|
||||
getWorkspacePlan,
|
||||
countSeatsByTypeInWorkspace,
|
||||
getWorkspacePlanProductId,
|
||||
reconcileSubscriptionData
|
||||
}: {
|
||||
getWorkspacePlan: GetWorkspacePlan
|
||||
countSeatsByTypeInWorkspace: CountSeatsByTypeInWorkspace
|
||||
getWorkspacePlanProductId: GetWorkspacePlanProductId
|
||||
reconcileSubscriptionData: ReconcileSubscriptionData
|
||||
}): DownscaleWorkspaceSubscription =>
|
||||
async ({ workspaceSubscription }) => {
|
||||
const workspaceId = workspaceSubscription.workspaceId
|
||||
|
||||
const workspacePlan = await getWorkspacePlan({ workspaceId })
|
||||
if (!workspacePlan) throw new WorkspacePlanNotFoundError()
|
||||
|
||||
switch (workspacePlan.name) {
|
||||
case 'team':
|
||||
case 'pro':
|
||||
break
|
||||
case 'starter':
|
||||
case 'plus':
|
||||
case 'business':
|
||||
case 'unlimited':
|
||||
case 'academia':
|
||||
case 'starterInvoiced':
|
||||
case 'plusInvoiced':
|
||||
case 'businessInvoiced':
|
||||
case 'free':
|
||||
throw new WorkspacePlanMismatchError()
|
||||
default:
|
||||
throwUncoveredError(workspacePlan)
|
||||
}
|
||||
|
||||
if (workspacePlan.status === 'canceled') return false
|
||||
|
||||
const editorsCount = await countSeatsByTypeInWorkspace({
|
||||
workspaceId,
|
||||
type: 'editor'
|
||||
})
|
||||
|
||||
const subscriptionData = cloneDeep(workspaceSubscription.subscriptionData)
|
||||
|
||||
mutateSubscriptionDataWithNewValidSeatNumbers({
|
||||
seatCount: editorsCount,
|
||||
workspacePlan: workspacePlan.name,
|
||||
getWorkspacePlanProductId,
|
||||
subscriptionData
|
||||
})
|
||||
|
||||
if (!isEqual(subscriptionData, workspaceSubscription.subscriptionData)) {
|
||||
await reconcileSubscriptionData({ subscriptionData, prorationBehavior: 'none' })
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const manageSubscriptionDownscaleFactoryOld =
|
||||
({
|
||||
getWorkspaceSubscriptions,
|
||||
downscaleWorkspaceSubscription,
|
||||
updateWorkspaceSubscription
|
||||
}: {
|
||||
getWorkspaceSubscriptions: GetWorkspaceSubscriptions
|
||||
downscaleWorkspaceSubscription: DownscaleWorkspaceSubscription
|
||||
updateWorkspaceSubscription: UpsertWorkspaceSubscription
|
||||
}) =>
|
||||
async (context: { logger: Logger }) => {
|
||||
const { logger } = context
|
||||
const subscriptions = await getWorkspaceSubscriptions()
|
||||
for (const workspaceSubscription of subscriptions) {
|
||||
const log = logger.child({ workspaceId: workspaceSubscription.workspaceId })
|
||||
try {
|
||||
const subDownscaled = await downscaleWorkspaceSubscription({
|
||||
workspaceSubscription
|
||||
})
|
||||
if (subDownscaled) {
|
||||
log.info(
|
||||
'Downscaled workspace subscription to match the current workspace team'
|
||||
)
|
||||
} else {
|
||||
log.info('Did not need to downscale the workspace subscription')
|
||||
}
|
||||
} catch (err) {
|
||||
log.error({ err }, 'Failed to downscale workspace subscription')
|
||||
}
|
||||
const newBillingCycleEnd = calculateNewBillingCycleEnd({ workspaceSubscription })
|
||||
const updatedWorkspaceSubscription = {
|
||||
...workspaceSubscription,
|
||||
currentBillingCycleEnd: newBillingCycleEnd
|
||||
}
|
||||
await updateWorkspaceSubscription({
|
||||
workspaceSubscription: updatedWorkspaceSubscription
|
||||
})
|
||||
log.info({ updatedWorkspaceSubscription }, 'Updated workspace billing cycle end')
|
||||
}
|
||||
}
|
||||
|
||||
export const manageSubscriptionDownscaleFactoryNew =
|
||||
({
|
||||
getWorkspaceSubscriptions,
|
||||
downscaleWorkspaceSubscription,
|
||||
updateWorkspaceSubscription,
|
||||
getSubscriptionData
|
||||
}: {
|
||||
getWorkspaceSubscriptions: GetWorkspaceSubscriptions
|
||||
downscaleWorkspaceSubscription: DownscaleWorkspaceSubscription
|
||||
updateWorkspaceSubscription: UpsertWorkspaceSubscription
|
||||
getSubscriptionData: GetSubscriptionData
|
||||
}) =>
|
||||
async (context: { logger: Logger }) => {
|
||||
const { logger } = context
|
||||
const subscriptions = await getWorkspaceSubscriptions()
|
||||
for (const workspaceSubscription of subscriptions) {
|
||||
const log = logger.child({ workspaceId: workspaceSubscription.workspaceId })
|
||||
try {
|
||||
//TODO:
|
||||
const subDownscaled = await downscaleWorkspaceSubscription({
|
||||
workspaceSubscription
|
||||
})
|
||||
if (subDownscaled) {
|
||||
log.info(
|
||||
'Downscaled workspace subscription to match the current workspace team'
|
||||
)
|
||||
} else {
|
||||
log.info('Did not need to downscale the workspace subscription')
|
||||
}
|
||||
} catch (err) {
|
||||
log.error({ err }, 'Failed to downscale workspace subscription')
|
||||
}
|
||||
const subscriptionData = await getSubscriptionData(
|
||||
workspaceSubscription.subscriptionData
|
||||
)
|
||||
const updatedWorkspaceSubscription = {
|
||||
...workspaceSubscription,
|
||||
currentBillingCycleEnd: subscriptionData.currentPeriodEnd
|
||||
}
|
||||
await updateWorkspaceSubscription({
|
||||
workspaceSubscription: updatedWorkspaceSubscription
|
||||
})
|
||||
log.info({ updatedWorkspaceSubscription }, 'Updated workspace billing cycle end')
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,9 @@ import { assign } from 'lodash'
|
||||
export const createTestSubscriptionData = (
|
||||
overrides: Partial<SubscriptionData> = {}
|
||||
): SubscriptionData => {
|
||||
const defaultValues: SubscriptionData = {
|
||||
const aMonthFromNow = new Date()
|
||||
aMonthFromNow.setMonth(new Date().getMonth() + 1)
|
||||
const defaultValues = {
|
||||
cancelAt: null,
|
||||
customerId: cryptoRandomString({ length: 10 }),
|
||||
products: [
|
||||
@@ -20,7 +22,8 @@ export const createTestSubscriptionData = (
|
||||
}
|
||||
],
|
||||
status: 'active',
|
||||
subscriptionId: cryptoRandomString({ length: 10 })
|
||||
subscriptionId: cryptoRandomString({ length: 10 }),
|
||||
currentPeriodEnd: aMonthFromNow.toISOString()
|
||||
}
|
||||
return assign(defaultValues, overrides)
|
||||
}
|
||||
|
||||
@@ -10,10 +10,11 @@ import {
|
||||
upsertPaidWorkspacePlanFactory,
|
||||
getWorkspaceSubscriptionFactory,
|
||||
getWorkspaceSubscriptionBySubscriptionIdFactory,
|
||||
getWorkspaceSubscriptionsPastBillingCycleEndFactory,
|
||||
changeExpiredTrialWorkspacePlanStatusesFactory,
|
||||
upsertTrialWorkspacePlanFactory,
|
||||
getWorkspacesByPlanAgeFactory
|
||||
getWorkspacesByPlanAgeFactory,
|
||||
getWorkspaceSubscriptionsPastBillingCycleEndFactoryOldPlans,
|
||||
upsertWorkspacePlanFactory
|
||||
} from '@/modules/gatekeeper/repositories/billing'
|
||||
import {
|
||||
createTestSubscriptionData,
|
||||
@@ -43,8 +44,8 @@ const getWorkspaceSubscription = getWorkspaceSubscriptionFactory({ db })
|
||||
const getWorkspaceSubscriptionBySubscriptionId =
|
||||
getWorkspaceSubscriptionBySubscriptionIdFactory({ db })
|
||||
|
||||
const getSubscriptionsAboutToEndBillingCycle =
|
||||
getWorkspaceSubscriptionsPastBillingCycleEndFactory({ db })
|
||||
const getSubscriptionsAboutToEndBillingCycleOld =
|
||||
getWorkspaceSubscriptionsPastBillingCycleEndFactoryOldPlans({ db })
|
||||
|
||||
const changeExpiredTrialWorkspacePlanStatuses =
|
||||
changeExpiredTrialWorkspacePlanStatusesFactory({ db })
|
||||
@@ -526,10 +527,18 @@ describe('billing repositories @gatekeeper', () => {
|
||||
const workspace2Subscription = createTestWorkspaceSubscription({
|
||||
workspaceId: workspace2Id
|
||||
})
|
||||
await upsertWorkspacePlanFactory({ db })({
|
||||
workspacePlan: {
|
||||
workspaceId: workspace2Subscription.workspaceId,
|
||||
name: 'plus',
|
||||
status: 'valid',
|
||||
createdAt: new Date()
|
||||
}
|
||||
})
|
||||
await upsertWorkspaceSubscription({
|
||||
workspaceSubscription: workspace2Subscription
|
||||
})
|
||||
const subscriptions = await getSubscriptionsAboutToEndBillingCycle()
|
||||
const subscriptions = await getSubscriptionsAboutToEndBillingCycleOld()
|
||||
expect(subscriptions).deep.equalInAnyOrder([workspace2Subscription])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -586,7 +586,8 @@ describe('checkout @gatekeeper', () => {
|
||||
}
|
||||
],
|
||||
status: 'active',
|
||||
cancelAt: null
|
||||
cancelAt: null,
|
||||
currentPeriodEnd: new Date()
|
||||
}
|
||||
|
||||
let storedWorkspaceSubscriptionData: WorkspaceSubscription | undefined =
|
||||
|
||||
@@ -15,10 +15,14 @@ import {
|
||||
import {
|
||||
addWorkspaceSubscriptionSeatIfNeededFactoryNew,
|
||||
addWorkspaceSubscriptionSeatIfNeededFactoryOld,
|
||||
downscaleWorkspaceSubscriptionFactory,
|
||||
handleSubscriptionUpdateFactory,
|
||||
manageSubscriptionDownscaleFactory
|
||||
handleSubscriptionUpdateFactory
|
||||
} from '@/modules/gatekeeper/services/subscriptions'
|
||||
import {
|
||||
downscaleWorkspaceSubscriptionFactoryNew,
|
||||
downscaleWorkspaceSubscriptionFactoryOld,
|
||||
manageSubscriptionDownscaleFactoryOld
|
||||
} from '@/modules/gatekeeper/services/subscriptions/manageSubscriptionDownscale'
|
||||
|
||||
import {
|
||||
createTestSubscriptionData,
|
||||
createTestWorkspaceSubscription
|
||||
@@ -1014,13 +1018,13 @@ describe('subscriptions @gatekeeper', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('downscaleWorkspaceSubscriptionFactory creates a function, that', () => {
|
||||
describe('downscaleWorkspaceSubscriptionFactoryOld creates a function, that', () => {
|
||||
it('throws an error if the workspace has no plan attached to it', async () => {
|
||||
const subscriptionData = createTestSubscriptionData()
|
||||
const workspaceSubscription = createTestWorkspaceSubscription({
|
||||
subscriptionData
|
||||
})
|
||||
const downscaleSubscription = downscaleWorkspaceSubscriptionFactory({
|
||||
const downscaleSubscription = downscaleWorkspaceSubscriptionFactoryOld({
|
||||
getWorkspacePlan: async () => null,
|
||||
countWorkspaceRole: async () => {
|
||||
expect.fail()
|
||||
@@ -1044,7 +1048,7 @@ describe('subscriptions @gatekeeper', () => {
|
||||
subscriptionData,
|
||||
workspaceId
|
||||
})
|
||||
const downscaleSubscription = downscaleWorkspaceSubscriptionFactory({
|
||||
const downscaleSubscription = downscaleWorkspaceSubscriptionFactoryOld({
|
||||
getWorkspacePlan: async () => ({
|
||||
name: 'unlimited',
|
||||
workspaceId,
|
||||
@@ -1073,7 +1077,7 @@ describe('subscriptions @gatekeeper', () => {
|
||||
subscriptionData,
|
||||
workspaceId
|
||||
})
|
||||
const downscaleSubscription = downscaleWorkspaceSubscriptionFactory({
|
||||
const downscaleSubscription = downscaleWorkspaceSubscriptionFactoryOld({
|
||||
getWorkspacePlan: async () => ({
|
||||
name: 'plus',
|
||||
workspaceId,
|
||||
@@ -1109,7 +1113,7 @@ describe('subscriptions @gatekeeper', () => {
|
||||
workspaceId
|
||||
})
|
||||
const workspacePlanName = 'plus'
|
||||
const downscaleSubscription = downscaleWorkspaceSubscriptionFactory({
|
||||
const downscaleSubscription = downscaleWorkspaceSubscriptionFactoryOld({
|
||||
getWorkspacePlan: async () => ({
|
||||
name: workspacePlanName,
|
||||
workspaceId,
|
||||
@@ -1164,7 +1168,7 @@ describe('subscriptions @gatekeeper', () => {
|
||||
const workspacePlanName = 'plus'
|
||||
|
||||
let reconciledSub: SubscriptionDataInput | undefined = undefined
|
||||
const downscaleSubscription = downscaleWorkspaceSubscriptionFactory({
|
||||
const downscaleSubscription = downscaleWorkspaceSubscriptionFactoryOld({
|
||||
getWorkspacePlan: async () => ({
|
||||
name: workspacePlanName,
|
||||
workspaceId,
|
||||
@@ -1193,14 +1197,178 @@ describe('subscriptions @gatekeeper', () => {
|
||||
).to.be.equal(guestQuantity / 2)
|
||||
})
|
||||
})
|
||||
describe('manageSubscriptionDownscaleFactory, creates a function, that', () => {
|
||||
describe('downscaleWorkspaceSubscriptionFactoryNew creates a function, that', () => {
|
||||
it('throws an error if the workspace has no plan attached to it', async () => {
|
||||
const subscriptionData = createTestSubscriptionData()
|
||||
const workspaceSubscription = createTestWorkspaceSubscription({
|
||||
subscriptionData
|
||||
})
|
||||
const downscaleSubscription = downscaleWorkspaceSubscriptionFactoryNew({
|
||||
getWorkspacePlan: async () => null,
|
||||
countSeatsByTypeInWorkspace: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
getWorkspacePlanProductId: () => {
|
||||
expect.fail()
|
||||
},
|
||||
reconcileSubscriptionData: async () => {
|
||||
expect.fail()
|
||||
}
|
||||
})
|
||||
const err = await expectToThrow(async () => {
|
||||
await downscaleSubscription({ workspaceSubscription })
|
||||
})
|
||||
expect(err.message).to.equal(new WorkspacePlanNotFoundError().message)
|
||||
})
|
||||
it('throws an error if workspacePlan is not a paid plan', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const subscriptionData = createTestSubscriptionData()
|
||||
const workspaceSubscription = createTestWorkspaceSubscription({
|
||||
subscriptionData,
|
||||
workspaceId
|
||||
})
|
||||
const downscaleSubscription = downscaleWorkspaceSubscriptionFactoryNew({
|
||||
getWorkspacePlan: async () => ({
|
||||
name: 'unlimited',
|
||||
workspaceId,
|
||||
createdAt: new Date(),
|
||||
status: 'valid'
|
||||
}),
|
||||
countSeatsByTypeInWorkspace: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
getWorkspacePlanProductId: () => {
|
||||
expect.fail()
|
||||
},
|
||||
reconcileSubscriptionData: async () => {
|
||||
expect.fail()
|
||||
}
|
||||
})
|
||||
const err = await expectToThrow(async () => {
|
||||
await downscaleSubscription({ workspaceSubscription })
|
||||
})
|
||||
expect(err.message).to.equal(new WorkspacePlanMismatchError().message)
|
||||
})
|
||||
it('returns if the subscription is canceled', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const subscriptionData = createTestSubscriptionData()
|
||||
const workspaceSubscription = createTestWorkspaceSubscription({
|
||||
subscriptionData,
|
||||
workspaceId
|
||||
})
|
||||
const downscaleSubscription = downscaleWorkspaceSubscriptionFactoryNew({
|
||||
getWorkspacePlan: async () => ({
|
||||
name: 'pro',
|
||||
workspaceId,
|
||||
createdAt: new Date(),
|
||||
status: 'canceled'
|
||||
}),
|
||||
countSeatsByTypeInWorkspace: async () => {
|
||||
expect.fail()
|
||||
},
|
||||
getWorkspacePlanProductId: () => {
|
||||
expect.fail()
|
||||
},
|
||||
reconcileSubscriptionData: async () => {
|
||||
expect.fail()
|
||||
}
|
||||
})
|
||||
const hasDownscaled = await downscaleSubscription({ workspaceSubscription })
|
||||
expect(hasDownscaled).to.be.false
|
||||
})
|
||||
it('does not reconcile the subscription seats did not change', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const priceId = cryptoRandomString({ length: 10 })
|
||||
const productId = cryptoRandomString({ length: 10 })
|
||||
const quantity = 10
|
||||
const subscriptionItemId = cryptoRandomString({ length: 10 })
|
||||
const subscriptionData = createTestSubscriptionData({
|
||||
products: [{ priceId, productId, quantity, subscriptionItemId }]
|
||||
})
|
||||
const workspaceSubscription = createTestWorkspaceSubscription({
|
||||
subscriptionData,
|
||||
billingInterval: 'monthly',
|
||||
currentBillingCycleEnd: new Date(2034, 11, 5),
|
||||
workspaceId
|
||||
})
|
||||
const workspacePlanName = 'pro'
|
||||
const downscaleSubscription = downscaleWorkspaceSubscriptionFactoryNew({
|
||||
getWorkspacePlan: async () => ({
|
||||
name: workspacePlanName,
|
||||
workspaceId,
|
||||
createdAt: new Date(),
|
||||
status: 'valid'
|
||||
}),
|
||||
countSeatsByTypeInWorkspace: async () => {
|
||||
return 10
|
||||
},
|
||||
getWorkspacePlanProductId: ({ workspacePlan }) => {
|
||||
return workspacePlan === workspacePlanName
|
||||
? productId
|
||||
: cryptoRandomString({ length: 10 })
|
||||
},
|
||||
reconcileSubscriptionData: async () => {
|
||||
expect.fail()
|
||||
}
|
||||
})
|
||||
await downscaleSubscription({ workspaceSubscription })
|
||||
})
|
||||
it('reconciles the subscription to the new seat values', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const proPriceId = cryptoRandomString({ length: 10 })
|
||||
const proProductId = cryptoRandomString({ length: 10 })
|
||||
const proQuantity = 10
|
||||
const proSubscriptionItemId = cryptoRandomString({ length: 10 })
|
||||
|
||||
const subscriptionData = createTestSubscriptionData({
|
||||
products: [
|
||||
{
|
||||
priceId: proPriceId,
|
||||
productId: proProductId,
|
||||
quantity: proQuantity,
|
||||
subscriptionItemId: proSubscriptionItemId
|
||||
}
|
||||
]
|
||||
})
|
||||
const testWorkspaceSubscription = createTestWorkspaceSubscription({
|
||||
subscriptionData,
|
||||
workspaceId
|
||||
})
|
||||
const workspacePlanName = 'pro'
|
||||
|
||||
let reconciledSub: SubscriptionDataInput | undefined = undefined
|
||||
const downscaleSubscription = downscaleWorkspaceSubscriptionFactoryNew({
|
||||
getWorkspacePlan: async () => ({
|
||||
name: workspacePlanName,
|
||||
workspaceId,
|
||||
createdAt: new Date(),
|
||||
status: 'valid'
|
||||
}),
|
||||
countSeatsByTypeInWorkspace: async () => {
|
||||
return 5
|
||||
},
|
||||
getWorkspacePlanProductId: () => {
|
||||
return proProductId
|
||||
},
|
||||
reconcileSubscriptionData: async ({ subscriptionData }) => {
|
||||
reconciledSub = subscriptionData
|
||||
}
|
||||
})
|
||||
await downscaleSubscription({ workspaceSubscription: testWorkspaceSubscription })
|
||||
|
||||
expect(
|
||||
reconciledSub!.products.find((p) => p.productId === proProductId)?.quantity
|
||||
).to.be.equal(5)
|
||||
})
|
||||
})
|
||||
describe('manageSubscriptionDownscaleFactoryOld, creates a function, that', () => {
|
||||
it('still updates the monthly billing cycle end, even if subscription reconciliation fails', async () => {
|
||||
const testWorkspaceSubscription = createTestWorkspaceSubscription({
|
||||
billingInterval: 'monthly',
|
||||
currentBillingCycleEnd: new Date(2034, 11, 5)
|
||||
})
|
||||
let updatedWorkspaceSubscription: WorkspaceSubscription | undefined = undefined
|
||||
await manageSubscriptionDownscaleFactory({
|
||||
await manageSubscriptionDownscaleFactoryOld({
|
||||
getWorkspaceSubscriptions: async () => [testWorkspaceSubscription],
|
||||
downscaleWorkspaceSubscription: async () => {
|
||||
throw new Error('kabumm')
|
||||
@@ -1222,7 +1390,7 @@ describe('subscriptions @gatekeeper', () => {
|
||||
currentBillingCycleEnd: new Date(2034, 11, 5)
|
||||
})
|
||||
let updatedWorkspaceSubscription: WorkspaceSubscription | undefined = undefined
|
||||
await manageSubscriptionDownscaleFactory({
|
||||
await manageSubscriptionDownscaleFactoryOld({
|
||||
getWorkspaceSubscriptions: async () => [testWorkspaceSubscription],
|
||||
downscaleWorkspaceSubscription: async () => {
|
||||
throw new Error('kabumm')
|
||||
@@ -1593,7 +1761,8 @@ describe('subscriptions @gatekeeper', () => {
|
||||
customerId: cryptoRandomString({ length: 10 }),
|
||||
subscriptionId: cryptoRandomString({ length: 10 }),
|
||||
status: 'active',
|
||||
products: []
|
||||
products: [],
|
||||
currentPeriodEnd: new Date()
|
||||
}
|
||||
const workspaceSubscription = createTestWorkspaceSubscription({
|
||||
subscriptionData
|
||||
@@ -1657,7 +1826,8 @@ describe('subscriptions @gatekeeper', () => {
|
||||
quantity: 20,
|
||||
subscriptionItemId: cryptoRandomString({ length: 10 })
|
||||
}
|
||||
]
|
||||
],
|
||||
currentPeriodEnd: new Date()
|
||||
}
|
||||
const workspaceSubscription = createTestWorkspaceSubscription({
|
||||
subscriptionData,
|
||||
@@ -2044,7 +2214,8 @@ describe('subscriptions @gatekeeper', () => {
|
||||
customerId: cryptoRandomString({ length: 10 }),
|
||||
subscriptionId: cryptoRandomString({ length: 10 }),
|
||||
status: 'active',
|
||||
products: []
|
||||
products: [],
|
||||
currentPeriodEnd: new Date()
|
||||
}
|
||||
const workspaceSubscription = createTestWorkspaceSubscription({
|
||||
subscriptionData
|
||||
@@ -2102,7 +2273,8 @@ describe('subscriptions @gatekeeper', () => {
|
||||
quantity: 10,
|
||||
subscriptionItemId: cryptoRandomString({ length: 10 })
|
||||
}
|
||||
]
|
||||
],
|
||||
currentPeriodEnd: new Date()
|
||||
}
|
||||
const workspaceSubscription = createTestWorkspaceSubscription({
|
||||
subscriptionData,
|
||||
|
||||
@@ -195,6 +195,8 @@ export const createTestWorkspace = async (
|
||||
}
|
||||
|
||||
if (addSubscription) {
|
||||
const aMonthFromNow = new Date()
|
||||
aMonthFromNow.setMonth(new Date().getMonth() + 1)
|
||||
await upsertSubscription({
|
||||
workspaceSubscription: {
|
||||
workspaceId: newWorkspace.id,
|
||||
@@ -207,7 +209,8 @@ export const createTestWorkspace = async (
|
||||
customerId: cryptoRandomString({ length: 10 }),
|
||||
cancelAt: null,
|
||||
status: 'active',
|
||||
products: []
|
||||
products: [],
|
||||
currentPeriodEnd: aMonthFromNow
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user