Fix: Various billing fixes (#3569)
This commit is contained in:
@@ -3559,8 +3559,9 @@ export type UpdateVersionInput = {
|
||||
};
|
||||
|
||||
export type UpgradePlanInput = {
|
||||
targetPlan: PaidWorkspacePlans;
|
||||
billingInterval: BillingInterval;
|
||||
workspaceId: Scalars['ID']['input'];
|
||||
workspacePlan: PaidWorkspacePlans;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -3540,8 +3540,9 @@ export type UpdateVersionInput = {
|
||||
};
|
||||
|
||||
export type UpgradePlanInput = {
|
||||
targetPlan: PaidWorkspacePlans;
|
||||
billingInterval: BillingInterval;
|
||||
workspaceId: Scalars['ID']['input'];
|
||||
workspacePlan: PaidWorkspacePlans;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -177,7 +177,7 @@ export const reconcileWorkspaceSubscriptionFactory =
|
||||
// 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: product.subscriptionItemId, deleted: true })
|
||||
items.push({ id: existingProduct.subscriptionItemId, deleted: true })
|
||||
} else {
|
||||
items.push({
|
||||
quantity: product.quantity,
|
||||
|
||||
@@ -26,7 +26,8 @@ import {
|
||||
getWorkspacePlanFactory,
|
||||
getWorkspaceSubscriptionFactory,
|
||||
saveCheckoutSessionFactory,
|
||||
upsertPaidWorkspacePlanFactory
|
||||
upsertPaidWorkspacePlanFactory,
|
||||
upsertWorkspaceSubscriptionFactory
|
||||
} from '@/modules/gatekeeper/repositories/billing'
|
||||
import { canWorkspaceAccessFeatureFactory } from '@/modules/gatekeeper/services/featureAuthorization'
|
||||
import { upgradeWorkspaceSubscriptionFactory } from '@/modules/gatekeeper/services/subscriptions'
|
||||
@@ -131,7 +132,7 @@ export = FF_GATEKEEPER_MODULE_ENABLED
|
||||
return session
|
||||
},
|
||||
upgradePlan: async (parent, args, ctx) => {
|
||||
const { workspaceId, targetPlan } = args.input
|
||||
const { workspaceId, workspacePlan, billingInterval } = args.input
|
||||
await authorizeResolver(
|
||||
ctx.userId,
|
||||
workspaceId,
|
||||
@@ -139,16 +140,22 @@ export = FF_GATEKEEPER_MODULE_ENABLED
|
||||
ctx.resourceAccessRules
|
||||
)
|
||||
const stripe = getStripeClient()
|
||||
|
||||
const countWorkspaceRole = countWorkspaceRoleWithOptionalProjectRoleFactory({
|
||||
db
|
||||
})
|
||||
await upgradeWorkspaceSubscriptionFactory({
|
||||
getWorkspacePlan: getWorkspacePlanFactory({ db }),
|
||||
reconcileSubscriptionData: reconcileWorkspaceSubscriptionFactory({
|
||||
stripe
|
||||
}),
|
||||
countWorkspaceRole,
|
||||
getWorkspaceSubscription: getWorkspaceSubscriptionFactory({ db }),
|
||||
getWorkspacePlanPrice,
|
||||
getWorkspacePlanProductId,
|
||||
upsertWorkspacePlan: upsertPaidWorkspacePlanFactory({ db })
|
||||
})({ workspaceId, targetPlan })
|
||||
upsertWorkspacePlan: upsertPaidWorkspacePlanFactory({ db }),
|
||||
updateWorkspaceSubscription: upsertWorkspaceSubscriptionFactory({ db })
|
||||
})({ workspaceId, targetPlan: workspacePlan, billingInterval })
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from '@/modules/gatekeeper/domain/billing'
|
||||
import {
|
||||
PaidWorkspacePlans,
|
||||
WorkspacePlanBillingIntervals,
|
||||
WorkspacePricingPlans
|
||||
} from '@/modules/gatekeeper/domain/workspacePricing'
|
||||
import {
|
||||
@@ -344,6 +345,8 @@ export const upgradeWorkspaceSubscriptionFactory =
|
||||
getWorkspacePlanPrice,
|
||||
getWorkspaceSubscription,
|
||||
reconcileSubscriptionData,
|
||||
updateWorkspaceSubscription,
|
||||
countWorkspaceRole,
|
||||
upsertWorkspacePlan
|
||||
}: {
|
||||
getWorkspacePlan: GetWorkspacePlan
|
||||
@@ -351,14 +354,18 @@ export const upgradeWorkspaceSubscriptionFactory =
|
||||
getWorkspacePlanPrice: GetWorkspacePlanPrice
|
||||
getWorkspaceSubscription: GetWorkspaceSubscription
|
||||
reconcileSubscriptionData: ReconcileSubscriptionData
|
||||
updateWorkspaceSubscription: UpsertWorkspaceSubscription
|
||||
countWorkspaceRole: CountWorkspaceRoleWithOptionalProjectRole
|
||||
upsertWorkspacePlan: UpsertPaidWorkspacePlan
|
||||
}) =>
|
||||
async ({
|
||||
workspaceId,
|
||||
targetPlan
|
||||
targetPlan,
|
||||
billingInterval
|
||||
}: {
|
||||
workspaceId: string
|
||||
targetPlan: PaidWorkspacePlans
|
||||
billingInterval: WorkspacePlanBillingIntervals
|
||||
}) => {
|
||||
const workspacePlan = await getWorkspacePlan({ workspaceId })
|
||||
|
||||
@@ -397,18 +404,68 @@ export const upgradeWorkspaceSubscriptionFactory =
|
||||
starter: 1
|
||||
}
|
||||
|
||||
if (planOrder[workspacePlan.name] >= planOrder[targetPlan])
|
||||
if (
|
||||
planOrder[workspacePlan.name] === planOrder[targetPlan] &&
|
||||
workspaceSubscription.billingInterval === billingInterval
|
||||
)
|
||||
throw new WorkspacePlanDowngradeError()
|
||||
if (planOrder[workspacePlan.name] > planOrder[targetPlan])
|
||||
throw new WorkspacePlanDowngradeError()
|
||||
|
||||
switch (billingInterval) {
|
||||
case 'monthly':
|
||||
if (workspaceSubscription.billingInterval === 'yearly')
|
||||
throw new WorkspacePlanDowngradeError()
|
||||
case 'yearly':
|
||||
break
|
||||
default:
|
||||
throwUncoveredError(billingInterval)
|
||||
}
|
||||
|
||||
const subscriptionData: SubscriptionDataInput = cloneDeep(
|
||||
workspaceSubscription.subscriptionData
|
||||
)
|
||||
|
||||
const product = subscriptionData.products.find(
|
||||
(p) =>
|
||||
p.productId === getWorkspacePlanProductId({ workspacePlan: workspacePlan.name })
|
||||
)
|
||||
if (!product) throw new WorkspacePlanMismatchError()
|
||||
const seatCount = product.quantity
|
||||
|
||||
const [guestCount, memberCount, adminCount] = await Promise.all([
|
||||
countWorkspaceRole({ workspaceId, workspaceRole: 'workspace:guest' }),
|
||||
countWorkspaceRole({ workspaceId, workspaceRole: 'workspace:member' }),
|
||||
countWorkspaceRole({ workspaceId, workspaceRole: 'workspace:admin' })
|
||||
])
|
||||
|
||||
workspaceSubscription.updatedAt = new Date()
|
||||
if (workspaceSubscription.billingInterval !== billingInterval) {
|
||||
workspaceSubscription.billingInterval = billingInterval
|
||||
workspaceSubscription.currentBillingCycleEnd = calculateNewBillingCycleEnd({
|
||||
workspaceSubscription
|
||||
})
|
||||
const guestProduct = subscriptionData.products.find(
|
||||
(p) => p.productId === getWorkspacePlanProductId({ workspacePlan: 'guest' })
|
||||
)
|
||||
if (guestProduct) {
|
||||
mutateSubscriptionDataWithNewValidSeatNumbers({
|
||||
seatCount: 0,
|
||||
getWorkspacePlanProductId,
|
||||
subscriptionData,
|
||||
workspacePlan: 'guest'
|
||||
})
|
||||
|
||||
subscriptionData.products.push({
|
||||
quantity: guestCount,
|
||||
productId: getWorkspacePlanProductId({ workspacePlan: 'guest' }),
|
||||
priceId: getWorkspacePlanPrice({
|
||||
workspacePlan: 'guest',
|
||||
billingInterval
|
||||
}),
|
||||
subscriptionItemId: undefined
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// set current plan seat count to 0
|
||||
mutateSubscriptionDataWithNewValidSeatNumbers({
|
||||
@@ -420,11 +477,11 @@ export const upgradeWorkspaceSubscriptionFactory =
|
||||
|
||||
// set target plan seat count to current seat count
|
||||
subscriptionData.products.push({
|
||||
quantity: seatCount,
|
||||
quantity: memberCount + adminCount,
|
||||
productId: getWorkspacePlanProductId({ workspacePlan: targetPlan }),
|
||||
priceId: getWorkspacePlanPrice({
|
||||
workspacePlan: targetPlan,
|
||||
billingInterval: workspaceSubscription.billingInterval
|
||||
billingInterval
|
||||
}),
|
||||
subscriptionItemId: undefined
|
||||
})
|
||||
@@ -438,4 +495,5 @@ export const upgradeWorkspaceSubscriptionFactory =
|
||||
createdAt: new Date()
|
||||
}
|
||||
})
|
||||
await updateWorkspaceSubscription({ workspaceSubscription })
|
||||
}
|
||||
|
||||
@@ -923,12 +923,19 @@ describe('subscriptions @gatekeeper', () => {
|
||||
},
|
||||
upsertWorkspacePlan: () => {
|
||||
expect.fail()
|
||||
},
|
||||
updateWorkspaceSubscription: () => {
|
||||
expect.fail()
|
||||
},
|
||||
countWorkspaceRole: () => {
|
||||
expect.fail()
|
||||
}
|
||||
})
|
||||
const err = await expectToThrow(async () => {
|
||||
await upgradeWorkspaceSubscription({
|
||||
workspaceId,
|
||||
targetPlan: 'business'
|
||||
targetPlan: 'business',
|
||||
billingInterval: 'monthly'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -958,12 +965,19 @@ describe('subscriptions @gatekeeper', () => {
|
||||
},
|
||||
upsertWorkspacePlan: () => {
|
||||
expect.fail()
|
||||
},
|
||||
updateWorkspaceSubscription: () => {
|
||||
expect.fail()
|
||||
},
|
||||
countWorkspaceRole: () => {
|
||||
expect.fail()
|
||||
}
|
||||
})
|
||||
const err = await expectToThrow(async () => {
|
||||
await upgradeWorkspaceSubscription({
|
||||
workspaceId,
|
||||
targetPlan: 'business'
|
||||
targetPlan: 'business',
|
||||
billingInterval: 'monthly'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -996,12 +1010,19 @@ describe('subscriptions @gatekeeper', () => {
|
||||
},
|
||||
upsertWorkspacePlan: () => {
|
||||
expect.fail()
|
||||
},
|
||||
updateWorkspaceSubscription: () => {
|
||||
expect.fail()
|
||||
},
|
||||
countWorkspaceRole: () => {
|
||||
expect.fail()
|
||||
}
|
||||
})
|
||||
const err = await expectToThrow(async () => {
|
||||
await upgradeWorkspaceSubscription({
|
||||
workspaceId,
|
||||
targetPlan: 'business'
|
||||
targetPlan: 'business',
|
||||
billingInterval: 'monthly'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1035,12 +1056,19 @@ describe('subscriptions @gatekeeper', () => {
|
||||
},
|
||||
upsertWorkspacePlan: () => {
|
||||
expect.fail()
|
||||
},
|
||||
updateWorkspaceSubscription: () => {
|
||||
expect.fail()
|
||||
},
|
||||
countWorkspaceRole: () => {
|
||||
expect.fail()
|
||||
}
|
||||
})
|
||||
const err = await expectToThrow(async () => {
|
||||
await upgradeWorkspaceSubscription({
|
||||
workspaceId,
|
||||
targetPlan: 'business'
|
||||
targetPlan: 'business',
|
||||
billingInterval: 'monthly'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1071,18 +1099,25 @@ describe('subscriptions @gatekeeper', () => {
|
||||
},
|
||||
upsertWorkspacePlan: () => {
|
||||
expect.fail()
|
||||
},
|
||||
updateWorkspaceSubscription: () => {
|
||||
expect.fail()
|
||||
},
|
||||
countWorkspaceRole: () => {
|
||||
expect.fail()
|
||||
}
|
||||
})
|
||||
const err = await expectToThrow(async () => {
|
||||
await upgradeWorkspaceSubscription({
|
||||
workspaceId,
|
||||
targetPlan: 'business'
|
||||
targetPlan: 'business',
|
||||
billingInterval: 'monthly'
|
||||
})
|
||||
})
|
||||
|
||||
expect(err.message).to.equal(new WorkspaceSubscriptionNotFoundError().message)
|
||||
})
|
||||
it('throws WorkspacePlanDowngradeError', async () => {
|
||||
it('throws WorkspacePlanDowngradeError for downgrading the plan', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const workspaceSubscription = createTestWorkspaceSubscription()
|
||||
const upgradeWorkspaceSubscription = upgradeWorkspaceSubscriptionFactory({
|
||||
@@ -1106,12 +1141,108 @@ describe('subscriptions @gatekeeper', () => {
|
||||
},
|
||||
upsertWorkspacePlan: () => {
|
||||
expect.fail()
|
||||
},
|
||||
updateWorkspaceSubscription: () => {
|
||||
expect.fail()
|
||||
},
|
||||
countWorkspaceRole: () => {
|
||||
expect.fail()
|
||||
}
|
||||
})
|
||||
const err = await expectToThrow(async () => {
|
||||
await upgradeWorkspaceSubscription({
|
||||
workspaceId,
|
||||
targetPlan: 'business'
|
||||
targetPlan: 'business',
|
||||
billingInterval: 'monthly'
|
||||
})
|
||||
})
|
||||
|
||||
expect(err.message).to.equal(new WorkspacePlanDowngradeError().message)
|
||||
})
|
||||
|
||||
it('throws WorkspacePlanDowngradeError for downgrading the billing interval', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const workspaceSubscription = createTestWorkspaceSubscription({
|
||||
billingInterval: 'yearly'
|
||||
})
|
||||
const upgradeWorkspaceSubscription = upgradeWorkspaceSubscriptionFactory({
|
||||
getWorkspacePlan: async () => ({
|
||||
workspaceId,
|
||||
createdAt: new Date(),
|
||||
name: 'business',
|
||||
status: 'valid'
|
||||
}),
|
||||
getWorkspacePlanProductId: () => {
|
||||
expect.fail()
|
||||
},
|
||||
getWorkspacePlanPrice: () => {
|
||||
expect.fail()
|
||||
},
|
||||
getWorkspaceSubscription: async () => {
|
||||
return workspaceSubscription
|
||||
},
|
||||
reconcileSubscriptionData: () => {
|
||||
expect.fail()
|
||||
},
|
||||
upsertWorkspacePlan: () => {
|
||||
expect.fail()
|
||||
},
|
||||
updateWorkspaceSubscription: () => {
|
||||
expect.fail()
|
||||
},
|
||||
countWorkspaceRole: () => {
|
||||
expect.fail()
|
||||
}
|
||||
})
|
||||
const err = await expectToThrow(async () => {
|
||||
await upgradeWorkspaceSubscription({
|
||||
workspaceId,
|
||||
targetPlan: 'business',
|
||||
billingInterval: 'monthly'
|
||||
})
|
||||
})
|
||||
|
||||
expect(err.message).to.equal(new WorkspacePlanDowngradeError().message)
|
||||
})
|
||||
it('throws WorkspacePlanDowngradeError for noop requests', async () => {
|
||||
const workspaceId = cryptoRandomString({ length: 10 })
|
||||
const workspaceSubscription = createTestWorkspaceSubscription({
|
||||
billingInterval: 'monthly'
|
||||
})
|
||||
const upgradeWorkspaceSubscription = upgradeWorkspaceSubscriptionFactory({
|
||||
getWorkspacePlan: async () => ({
|
||||
workspaceId,
|
||||
createdAt: new Date(),
|
||||
name: 'business',
|
||||
status: 'valid'
|
||||
}),
|
||||
getWorkspacePlanProductId: () => {
|
||||
expect.fail()
|
||||
},
|
||||
getWorkspacePlanPrice: () => {
|
||||
expect.fail()
|
||||
},
|
||||
getWorkspaceSubscription: async () => {
|
||||
return workspaceSubscription
|
||||
},
|
||||
reconcileSubscriptionData: () => {
|
||||
expect.fail()
|
||||
},
|
||||
upsertWorkspacePlan: () => {
|
||||
expect.fail()
|
||||
},
|
||||
updateWorkspaceSubscription: () => {
|
||||
expect.fail()
|
||||
},
|
||||
countWorkspaceRole: () => {
|
||||
expect.fail()
|
||||
}
|
||||
})
|
||||
const err = await expectToThrow(async () => {
|
||||
await upgradeWorkspaceSubscription({
|
||||
workspaceId,
|
||||
targetPlan: 'business',
|
||||
billingInterval: 'monthly'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1150,12 +1281,19 @@ describe('subscriptions @gatekeeper', () => {
|
||||
},
|
||||
upsertWorkspacePlan: () => {
|
||||
expect.fail()
|
||||
},
|
||||
updateWorkspaceSubscription: () => {
|
||||
expect.fail()
|
||||
},
|
||||
countWorkspaceRole: () => {
|
||||
expect.fail()
|
||||
}
|
||||
})
|
||||
const err = await expectToThrow(async () => {
|
||||
await upgradeWorkspaceSubscription({
|
||||
workspaceId,
|
||||
targetPlan: 'business'
|
||||
targetPlan: 'business',
|
||||
billingInterval: 'monthly'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1184,11 +1322,13 @@ describe('subscriptions @gatekeeper', () => {
|
||||
]
|
||||
}
|
||||
const workspaceSubscription = createTestWorkspaceSubscription({
|
||||
subscriptionData
|
||||
subscriptionData,
|
||||
billingInterval: 'monthly'
|
||||
})
|
||||
|
||||
let reconciledSubscriptionData: SubscriptionDataInput | undefined = undefined
|
||||
let updatedWorkspacePlan: WorkspacePlan | undefined = undefined
|
||||
let updatedWorkspaceSubscription: WorkspaceSubscription | undefined = undefined
|
||||
const upgradeWorkspaceSubscription = upgradeWorkspaceSubscriptionFactory({
|
||||
getWorkspacePlan: async () => ({
|
||||
workspaceId,
|
||||
@@ -1219,27 +1359,36 @@ describe('subscriptions @gatekeeper', () => {
|
||||
},
|
||||
upsertWorkspacePlan: async ({ workspacePlan }) => {
|
||||
updatedWorkspacePlan = workspacePlan
|
||||
},
|
||||
updateWorkspaceSubscription: async ({ workspaceSubscription }) => {
|
||||
updatedWorkspaceSubscription = workspaceSubscription
|
||||
},
|
||||
countWorkspaceRole: async () => {
|
||||
return 4
|
||||
}
|
||||
})
|
||||
await upgradeWorkspaceSubscription({
|
||||
workspaceId,
|
||||
targetPlan: 'business'
|
||||
targetPlan: 'business',
|
||||
billingInterval: 'yearly'
|
||||
})
|
||||
|
||||
expect(updatedWorkspacePlan!.name).to.equal('business')
|
||||
|
||||
expect(reconciledSubscriptionData!.products.length).to.equal(2)
|
||||
|
||||
expect(updatedWorkspaceSubscription!.billingInterval === 'yearly')
|
||||
|
||||
expect(
|
||||
reconciledSubscriptionData!.products.find(
|
||||
(p) => p.productId === 'guestProduct'
|
||||
)!.quantity
|
||||
).to.equal(10)
|
||||
).to.equal(4)
|
||||
const newProduct = reconciledSubscriptionData!.products.find(
|
||||
(p) => p.productId === 'businessProduct'
|
||||
)
|
||||
|
||||
expect(newProduct!.quantity).to.equal(20)
|
||||
expect(newProduct!.quantity).to.equal(8)
|
||||
expect(newProduct!.priceId).to.equal('newPlanPrice')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user